use std::process::Stdio;
use std::sync::{Arc, LazyLock};
use regex::Regex;
use tokio::io::AsyncBufReadExt as _;
use tokio::process::{Child as TokioChild, ChildStderr, ChildStdout, Command as TokioCommand};
use crate::BoxedFuture;
use crate::cli::{ProgressBar, ProgressBarBuilder};
use crate::lv::Lv;
use crate::process::{Command, Preset, pio};
#[inline(always)]
pub fn cargo(progress_message: impl Into<String>) -> Cargo {
Cargo {
error_lv: Lv::Error,
warning_lv: Lv::Warn,
other_lv: Lv::Debug,
diagnostic_hook: None,
progress_builder: crate::progress(progress_message),
}
}
pub struct Cargo {
error_lv: Lv,
warning_lv: Lv,
other_lv: Lv,
diagnostic_hook: Option<DianogsticHook>,
progress_builder: ProgressBarBuilder,
}
impl Cargo {
pub fn error(mut self, lv: Lv) -> Self {
self.error_lv = lv;
self
}
pub fn warning(mut self, lv: Lv) -> Self {
self.warning_lv = lv;
self
}
pub fn other(mut self, lv: Lv) -> Self {
self.other_lv = lv;
self
}
pub fn on_diagnostic<F: Fn(bool, &str) + Send + 'static>(mut self, f: F) -> Self {
self.diagnostic_hook = Some(Box::new(f));
self
}
#[inline(always)]
pub fn configure_spinner<F: FnOnce(ProgressBarBuilder) -> ProgressBarBuilder>(
mut self,
f: F,
) -> Self {
self.progress_builder = f(self.progress_builder);
self
}
}
impl Preset for Cargo {
type Output = Command<Cargo, CargoStubStdErr, pio::Null>;
fn configure<O, E, I>(self, command: crate::Command<O, E, I>) -> Self::Output {
command
.args(["--message-format=json-diagnostic-rendered-ansi"])
.stderr(CargoStubStdErr)
.stdout(self)
.stdin_null()
}
}
pub struct CargoTask {
error_lv: Lv,
warning_lv: Lv,
other_lv: Lv,
bar: Arc<ProgressBar>,
out: ChildStdout,
err: ChildStderr,
diagnostic_hook: Option<DianogsticHook>,
}
impl pio::ChildOutConfig for Cargo {
type Task = CargoTask;
type __Null = super::__OCNonNull;
fn configure_stdout(&mut self, command: &mut TokioCommand) {
command.stdout(Stdio::piped());
}
fn configure_stderr(&mut self, _: &mut TokioCommand) {}
fn take(self, child: &mut TokioChild, _: Option<&str>, _: bool) -> crate::Result<Self::Task> {
let stdout = super::take_child_stdout(child)?;
let stderr = super::take_child_stderr(child)?;
let bar = self.progress_builder.spawn();
Ok(CargoTask {
error_lv: self.error_lv,
warning_lv: self.warning_lv,
other_lv: self.other_lv,
bar,
out: stdout,
err: stderr,
diagnostic_hook: self.diagnostic_hook,
})
}
}
pub struct CargoStubStdErr;
impl pio::ChildOutConfig for CargoStubStdErr {
type Task = ();
type __Null = super::__OCNull;
fn configure_stdout(&mut self, _: &mut TokioCommand) {}
fn configure_stderr(&mut self, command: &mut TokioCommand) {
command.stderr(Stdio::piped());
}
fn take(self, _: &mut TokioChild, _: Option<&str>, _: bool) -> crate::Result<Self::Task> {
Ok(())
}
}
impl pio::ChildOutTask for CargoTask {
type Output = Arc<ProgressBar>;
fn run(self) -> (Option<BoxedFuture<()>>, Self::Output) {
let bar = Arc::clone(&self.bar);
(Some(Box::pin(self.main())), bar)
}
}
impl CargoTask {
async fn main(self) {
let read_out = tokio::io::BufReader::new(self.out);
let mut out_lines = Some(read_out.lines());
let read_err = tokio::io::BufReader::new(self.err);
let mut err_lines = Some(read_err.lines());
let bar = self.bar;
crate::progress!(bar, "preparing");
let mut state = PrintState::new(
self.error_lv,
self.warning_lv,
self.other_lv,
bar,
self.diagnostic_hook,
);
loop {
let read_res = match (&mut out_lines, &mut err_lines) {
(None, None) => break,
(Some(out), None) => Ok(out.next_line().await),
(None, Some(err)) => Err(err.next_line().await),
(Some(out), Some(err)) => {
tokio::select! {
x = out.next_line() => Ok(x),
x = err.next_line() => Err(x)
}
}
};
let line: Result<String, String> = match read_res {
Ok(x) => match x {
Ok(Some(x)) => Ok(x),
_ => {
out_lines = None;
continue;
}
},
Err(x) => match x {
Ok(Some(x)) => Err(x),
_ => {
err_lines = None;
continue;
}
},
};
match line {
Ok(line) => state.handle_stdout(&line),
Err(line) => state.handle_stderr(&line),
}
}
}
}
struct PrintState {
error_lv: Lv,
warning_lv: Lv,
other_lv: Lv,
bar: Arc<ProgressBar>,
done_count: usize,
in_progress: Vec<String>, buf: String,
diagnostic_hook: Option<DianogsticHook>,
stderr_printing_message_lv: Option<Lv>,
}
impl PrintState {
fn new(
error_lv: Lv,
warning_lv: Lv,
other_lv: Lv,
bar: Arc<ProgressBar>,
diagnostic_hook: Option<DianogsticHook>,
) -> Self {
Self {
error_lv,
warning_lv,
other_lv,
bar,
done_count: 0,
in_progress: Default::default(),
buf: Default::default(),
diagnostic_hook,
stderr_printing_message_lv: None,
}
}
fn handle_stdout(&mut self, line: &str) {
if !line.starts_with('{') {
crate::trace!("{line}");
return;
}
let payload = match crate::json::parse::<Payload>(line) {
Ok(x) => x,
Err(e) => {
crate::trace!("failed to parse cargo json output: {e:?}");
return;
}
};
match payload.reason {
"compiler-artifact" => {
let Some(target) = payload.target else {
return;
};
if target.name == "build-script-build" {
return;
}
self.done_count += 1;
self.in_progress.retain(|x| x != target.name);
self.update_bar();
}
"compiler-message" => {
let Some(message) = payload.message else {
return;
};
let Some(rendered) = message.rendered else {
return;
};
match message.level {
Some("warning") => match &self.diagnostic_hook {
None => {
crate::cli::__print_with_level(
self.warning_lv,
format_args!("{rendered}"),
);
}
Some(hook) => hook(true, &rendered),
},
Some("error") => match &self.diagnostic_hook {
None => {
crate::cli::__print_with_level(
self.error_lv,
format_args!("{rendered}"),
);
}
Some(hook) => hook(false, &rendered),
},
_ => {
crate::cli::__print_with_level(self.other_lv, format_args!("{rendered}"));
}
}
}
"build-finished" => match payload.success {
Some(true) => {
self.bar.done_by_ref();
crate::trace!("cargo build successful");
}
_ => {
crate::trace!("cargo build failed");
}
},
"build-script-executed" => {}
_ => {
crate::trace!("unhandled cargo message reason: {}", payload.reason);
}
}
}
fn handle_stderr(&mut self, line: &str) {
static STATUS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new("^((\x1b[^m]*m)|\\s)*(Compiling|Checking)((\x1b[^m]*m)|\\s)*").unwrap()
});
static ERROR_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new("^((\x1b[^m]*m)|\\s)*error").unwrap());
static WARNING_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new("^((\x1b[^m]*m)|\\s)*warning").unwrap());
let Some(m) = STATUS_REGEX.find(line) else {
if let Some(lv) = self.stderr_printing_message_lv {
crate::cli::__print_with_level(lv, format_args!("{line}"));
return;
}
if ERROR_REGEX.is_match(line) {
crate::cli::__print_with_level(self.error_lv, format_args!("{line}"));
self.stderr_printing_message_lv = Some(self.error_lv);
return;
}
if WARNING_REGEX.is_match(line) {
crate::cli::__print_with_level(self.warning_lv, format_args!("{line}"));
self.stderr_printing_message_lv = Some(self.warning_lv);
return;
}
crate::cli::__print_with_level(self.other_lv, format_args!("{line}"));
return;
};
crate::cli::__print_with_level(self.other_lv, format_args!("{line}"));
self.stderr_printing_message_lv = None;
let line = &line[m.end()..].trim();
let crate_name = match line.find(' ') {
None => line,
Some(i) => &line[..i],
};
self.in_progress.push(crate_name.replace('-', "_"));
self.update_bar();
}
fn update_bar(&mut self) {
let count = self.done_count;
let bar = &self.bar;
self.buf.clear();
let mut iter = self.in_progress.iter();
if let Some(x) = iter.next() {
self.buf.push_str(x);
for c in iter {
self.buf.push_str(", ");
self.buf.push_str(c);
}
crate::progress!(bar, "{count} done, compiling: {}", self.buf);
} else if count != 0 {
crate::progress!(bar, "{count} done");
}
}
}
type DianogsticHook = Box<dyn Fn(bool, &str) + Send>;
#[derive(serde::Deserialize)]
struct Payload<'a> {
reason: &'a str,
target: Option<PayloadTarget<'a>>,
message: Option<PayloadMessage<'a>>,
success: Option<bool>,
}
#[derive(serde::Deserialize)]
struct PayloadTarget<'a> {
name: &'a str,
}
#[derive(serde::Deserialize)]
struct PayloadMessage<'a> {
level: Option<&'a str>,
rendered: Option<String>,
}