Skip to main content

pistonite_cu/process/pio/
cargo_preset.rs

1use std::process::Stdio;
2use std::sync::{Arc, LazyLock};
3
4use regex::Regex;
5use tokio::io::AsyncBufReadExt as _;
6use tokio::process::{Child as TokioChild, ChildStderr, ChildStdout, Command as TokioCommand};
7
8use crate::BoxedFuture;
9use crate::cli::{ProgressBar, ProgressBarBuilder};
10use crate::lv::Lv;
11use crate::process::{Command, Preset, pio};
12
13/// Display progress of cargo task with a progress bar, and emitting
14/// status messages and diagnostic messages using this crate's printing utilities.
15///
16/// `json` feature is required to enable parsing cargo's output messages.
17///
18/// ```rust,no_run
19/// # use pistonite_cu as cu;
20/// use cu::pre::*;
21///
22/// # fn main() -> cu::Result<()> {
23/// cu::which("cargo")?.command()
24///     .args(["build", "--release"])
25///     .preset(cu::pio::cargo("building my crate"))
26///     .spawn()?.0
27///     .wait_nz()?;
28/// # Ok(()) }
29/// ```
30///
31/// # Behavior
32/// - Added args: `--message-format json-diagnostic-rendered-ansi`
33/// - All IO will be configured. You should avoid configuring IO by yourself
34///   before or after applying this preset. This may become enforced in the future
35///   through generics.
36///
37/// The progress is displayed on the progress bar, showing the current
38/// crates being built in one line (similar to the build progress bar shown
39/// by cargo).
40///
41/// You can customize the spawned progress bar with
42///
43/// # Message levels
44/// Errors, warnings and status messages (like `Compiling foobar v0.1.0`)
45/// can be configured with the [`error`](Cargo::error), [`warning`](Cargo::warning),
46/// or [`other`](Cargo::other) functions that take a message level.
47///
48/// ```rust,no_run
49/// # use pistonite_cu as cu;
50/// use cu::pre::*;
51///
52/// cu::pio::cargo("cargo build")
53///     // configure message levels; levels shown here are the default
54///     .error(cu::lv::E)
55///     .warning(cu::lv::W)
56///     .other(cu::lv::D);
57/// ```
58///
59/// # Diagnostic hooks
60/// To process diagnostic messages from cargo, you can provide a diagnostic hook,
61/// which is a function `(is_warning: bool, message: &str) -> ()`.
62/// If a diagnostic hook is provided, then the hook is responsible for displaying
63/// the message. The `error` and `warning` levels will have no effect.
64///
65/// ```rust,no_run
66/// # use pistonite_cu as cu;
67/// use cu::pre::*;
68///
69/// cu::pio::cargo("cargo build")
70///     // configure message levels; levels shown here are the default
71///     .on_diagnostic(|is_warning, message| {
72///         // this implementation will be identical to the default behavior
73///         if is_warning {
74///             cu::warn!("{message}")
75///         } else {
76///             cu::error!("{message}")
77///         }
78///     });
79/// ```
80///
81/// # Output
82/// The handle to the progress bar is emitted to the stdout slot.
83/// Be sure to manually call `.done()` on it. See [Progress Bars](fn@crate::progress)
84/// for more details
85///
86#[inline(always)]
87pub fn cargo(progress_message: impl Into<String>) -> Cargo {
88    Cargo {
89        error_lv: Lv::Error,
90        warning_lv: Lv::Warn,
91        other_lv: Lv::Debug,
92        diagnostic_hook: None,
93        progress_builder: crate::progress(progress_message),
94    }
95}
96pub struct Cargo {
97    error_lv: Lv,
98    warning_lv: Lv,
99    other_lv: Lv,
100    diagnostic_hook: Option<DianogsticHook>,
101    progress_builder: ProgressBarBuilder,
102}
103
104impl Cargo {
105    /// Set the level for printing error messages from cargo
106    pub fn error(mut self, lv: Lv) -> Self {
107        self.error_lv = lv;
108        self
109    }
110    /// Set the level for printing warning messages from cargo
111    pub fn warning(mut self, lv: Lv) -> Self {
112        self.warning_lv = lv;
113        self
114    }
115    /// Set the level for printing other messages from cargo
116    pub fn other(mut self, lv: Lv) -> Self {
117        self.other_lv = lv;
118        self
119    }
120    /// Set a diagnostic hook, used to inspect compiler diagnostics from cargo
121    ///
122    /// The parameters are `(is_warning, message)`. The message is ansi-rendered.
123    ///
124    /// The hook should take care of printing the message
125    pub fn on_diagnostic<F: Fn(bool, &str) + Send + 'static>(mut self, f: F) -> Self {
126        self.diagnostic_hook = Some(Box::new(f));
127        self
128    }
129
130    /// Configure the progress bar that will be spawned
131    #[inline(always)]
132    pub fn configure_spinner<F: FnOnce(ProgressBarBuilder) -> ProgressBarBuilder>(
133        mut self,
134        f: F,
135    ) -> Self {
136        self.progress_builder = f(self.progress_builder);
137        self
138    }
139}
140
141impl Preset for Cargo {
142    type Output = Command<Cargo, CargoStubStdErr, pio::Null>;
143
144    fn configure<O, E, I>(self, command: crate::Command<O, E, I>) -> Self::Output {
145        command
146            .args(["--message-format=json-diagnostic-rendered-ansi"])
147            .stderr(CargoStubStdErr)
148            .stdout(self)
149            .stdin_null()
150    }
151}
152
153pub struct CargoTask {
154    error_lv: Lv,
155    warning_lv: Lv,
156    other_lv: Lv,
157    bar: Arc<ProgressBar>,
158    out: ChildStdout,
159    err: ChildStderr,
160    diagnostic_hook: Option<DianogsticHook>,
161}
162
163impl pio::ChildOutConfig for Cargo {
164    type Task = CargoTask;
165    type __Null = super::__OCNonNull;
166    fn configure_stdout(&mut self, command: &mut TokioCommand) {
167        command.stdout(Stdio::piped());
168    }
169    fn configure_stderr(&mut self, _: &mut TokioCommand) {}
170    fn take(self, child: &mut TokioChild, _: Option<&str>, _: bool) -> crate::Result<Self::Task> {
171        let stdout = super::take_child_stdout(child)?;
172        let stderr = super::take_child_stderr(child)?;
173        let bar = self.progress_builder.spawn();
174        Ok(CargoTask {
175            error_lv: self.error_lv,
176            warning_lv: self.warning_lv,
177            other_lv: self.other_lv,
178            bar,
179            out: stdout,
180            err: stderr,
181            diagnostic_hook: self.diagnostic_hook,
182        })
183    }
184}
185pub struct CargoStubStdErr;
186impl pio::ChildOutConfig for CargoStubStdErr {
187    type Task = ();
188    type __Null = super::__OCNull;
189    fn configure_stdout(&mut self, _: &mut TokioCommand) {}
190    fn configure_stderr(&mut self, command: &mut TokioCommand) {
191        command.stderr(Stdio::piped());
192    }
193    fn take(self, _: &mut TokioChild, _: Option<&str>, _: bool) -> crate::Result<Self::Task> {
194        Ok(())
195    }
196}
197
198impl pio::ChildOutTask for CargoTask {
199    type Output = Arc<ProgressBar>;
200
201    fn run(self) -> (Option<BoxedFuture<()>>, Self::Output) {
202        let bar = Arc::clone(&self.bar);
203        (Some(Box::pin(self.main())), bar)
204    }
205}
206
207impl CargoTask {
208    async fn main(self) {
209        let read_out = tokio::io::BufReader::new(self.out);
210        let mut out_lines = Some(read_out.lines());
211        let read_err = tokio::io::BufReader::new(self.err);
212        let mut err_lines = Some(read_err.lines());
213
214        let bar = self.bar;
215
216        crate::progress!(bar, "preparing");
217
218        let mut state = PrintState::new(
219            self.error_lv,
220            self.warning_lv,
221            self.other_lv,
222            bar,
223            self.diagnostic_hook,
224        );
225
226        loop {
227            let read_res = match (&mut out_lines, &mut err_lines) {
228                (None, None) => break,
229                (Some(out), None) => Ok(out.next_line().await),
230                (None, Some(err)) => Err(err.next_line().await),
231                (Some(out), Some(err)) => {
232                    tokio::select! {
233                        x = out.next_line() => Ok(x),
234                        x = err.next_line() => Err(x)
235                    }
236                }
237            };
238            let line: Result<String, String> = match read_res {
239                Ok(x) => match x {
240                    Ok(Some(x)) => Ok(x),
241                    _ => {
242                        out_lines = None;
243                        continue;
244                    }
245                },
246                Err(x) => match x {
247                    Ok(Some(x)) => Err(x),
248                    _ => {
249                        err_lines = None;
250                        continue;
251                    }
252                },
253            };
254            match line {
255                Ok(line) => state.handle_stdout(&line),
256                Err(line) => state.handle_stderr(&line),
257            }
258        }
259    }
260}
261
262struct PrintState {
263    error_lv: Lv,
264    warning_lv: Lv,
265    other_lv: Lv,
266    bar: Arc<ProgressBar>,
267    done_count: usize,
268    in_progress: Vec<String>, // using a vec to preserve order
269    buf: String,
270    diagnostic_hook: Option<DianogsticHook>,
271    stderr_printing_message_lv: Option<Lv>,
272}
273
274impl PrintState {
275    fn new(
276        error_lv: Lv,
277        warning_lv: Lv,
278        other_lv: Lv,
279        bar: Arc<ProgressBar>,
280        diagnostic_hook: Option<DianogsticHook>,
281    ) -> Self {
282        Self {
283            error_lv,
284            warning_lv,
285            other_lv,
286            bar,
287            done_count: 0,
288            in_progress: Default::default(),
289            buf: Default::default(),
290            diagnostic_hook,
291            stderr_printing_message_lv: None,
292        }
293    }
294    fn handle_stdout(&mut self, line: &str) {
295        // only handle json output from stdout
296        if !line.starts_with('{') {
297            crate::trace!("{line}");
298            return;
299        }
300
301        let payload = match crate::json::parse::<Payload>(line) {
302            Ok(x) => x,
303            Err(e) => {
304                crate::trace!("failed to parse cargo json output: {e:?}");
305                return;
306            }
307        };
308        match payload.reason {
309            "compiler-artifact" => {
310                let Some(target) = payload.target else {
311                    return;
312                };
313                if target.name == "build-script-build" {
314                    // skip processing build script builds
315                    return;
316                }
317                self.done_count += 1;
318                self.in_progress.retain(|x| x != target.name);
319                self.update_bar();
320            }
321            "compiler-message" => {
322                let Some(message) = payload.message else {
323                    return;
324                };
325                let Some(rendered) = message.rendered else {
326                    return;
327                };
328                match message.level {
329                    Some("warning") => match &self.diagnostic_hook {
330                        None => {
331                            crate::cli::__print_with_level(
332                                self.warning_lv,
333                                format_args!("{rendered}"),
334                            );
335                        }
336                        Some(hook) => hook(true, &rendered),
337                    },
338                    Some("error") => match &self.diagnostic_hook {
339                        None => {
340                            crate::cli::__print_with_level(
341                                self.error_lv,
342                                format_args!("{rendered}"),
343                            );
344                        }
345                        Some(hook) => hook(false, &rendered),
346                    },
347                    _ => {
348                        crate::cli::__print_with_level(self.other_lv, format_args!("{rendered}"));
349                    }
350                }
351            }
352            "build-finished" => match payload.success {
353                Some(true) => {
354                    self.bar.done_by_ref();
355                    crate::trace!("cargo build successful");
356                }
357                _ => {
358                    crate::trace!("cargo build failed");
359                }
360            },
361            "build-script-executed" => {}
362            _ => {
363                crate::trace!("unhandled cargo message reason: {}", payload.reason);
364            }
365        }
366    }
367
368    fn handle_stderr(&mut self, line: &str) {
369        static STATUS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
370            Regex::new("^((\x1b[^m]*m)|\\s)*(Compiling|Checking)((\x1b[^m]*m)|\\s)*").unwrap()
371        });
372        static OTHER_STATUS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
373            Regex::new("^((\x1b[^m]*m)|\\s)*(Downloading|Downloaded)((\x1b[^m]*m)|\\s)*").unwrap()
374        });
375        static ERROR_REGEX: LazyLock<Regex> =
376            LazyLock::new(|| Regex::new("^((\x1b[^m]*m)|\\s)*error").unwrap());
377        static WARNING_REGEX: LazyLock<Regex> =
378            LazyLock::new(|| Regex::new("^((\x1b[^m]*m)|\\s)*warning").unwrap());
379        let Some(m) = STATUS_REGEX.find(line) else {
380            if OTHER_STATUS_REGEX.is_match(line) {
381                crate::cli::__print_with_level(self.other_lv, format_args!("{line}"));
382                self.stderr_printing_message_lv = None;
383                return;
384            }
385            // some error/warning messages aren't emited to stdout,
386            // so we use a regex to match and print them
387            if let Some(lv) = self.stderr_printing_message_lv {
388                // since the message might be multi-line, we
389                // keep printing until a status message is matched
390                crate::cli::__print_with_level(lv, format_args!("{line}"));
391                return;
392            }
393            // check if the message matches error/warning
394            if ERROR_REGEX.is_match(line) {
395                crate::cli::__print_with_level(self.error_lv, format_args!("{line}"));
396                self.stderr_printing_message_lv = Some(self.error_lv);
397                return;
398            }
399            if WARNING_REGEX.is_match(line) {
400                crate::cli::__print_with_level(self.warning_lv, format_args!("{line}"));
401                self.stderr_printing_message_lv = Some(self.warning_lv);
402                return;
403            }
404            // print as other message
405            crate::cli::__print_with_level(self.other_lv, format_args!("{line}"));
406            return;
407        };
408        // print the status message as other, and clear the error/warning message state
409        crate::cli::__print_with_level(self.other_lv, format_args!("{line}"));
410        self.stderr_printing_message_lv = None;
411
412        // process the status message
413        let line = &line[m.end()..].trim();
414        // crate name can't have space (right?)
415        let crate_name = match line.find(' ') {
416            None => line,
417            Some(i) => &line[..i],
418        };
419        self.in_progress.push(crate_name.replace('-', "_"));
420        self.update_bar();
421    }
422
423    fn update_bar(&mut self) {
424        let count = self.done_count;
425        let bar = &self.bar;
426
427        self.buf.clear();
428        let mut iter = self.in_progress.iter();
429        if let Some(x) = iter.next() {
430            self.buf.push_str(x);
431            for c in iter {
432                self.buf.push_str(", ");
433                self.buf.push_str(c);
434            }
435            crate::progress!(bar, "{count} done, compiling: {}", self.buf);
436        } else if count != 0 {
437            crate::progress!(bar, "{count} done");
438        }
439    }
440}
441
442// (is_warning, message) -> Break = don't print, Continue = print original or overriden message
443type DianogsticHook = Box<dyn Fn(bool, &str) + Send>;
444
445#[derive(serde::Deserialize)]
446struct Payload<'a> {
447    reason: &'a str,
448    target: Option<PayloadTarget<'a>>,
449    message: Option<PayloadMessage<'a>>,
450    success: Option<bool>,
451}
452
453#[derive(serde::Deserialize)]
454struct PayloadTarget<'a> {
455    name: &'a str,
456}
457
458#[derive(serde::Deserialize)]
459struct PayloadMessage<'a> {
460    level: Option<&'a str>,
461    // for some reason, this can't be deserialize as borrowed
462    rendered: Option<String>,
463}