rattler_build/
console_utils.rs

1//! This module contains utilities for logging and progress bar handling.
2use std::{
3    borrow::Cow,
4    collections::HashMap,
5    future::Future,
6    io,
7    str::FromStr,
8    sync::{Arc, Mutex},
9    time::{Duration, Instant},
10};
11
12use clap_verbosity_flag::{InfoLevel, Verbosity};
13use console::style;
14use indicatif::{
15    HumanBytes, HumanDuration, MultiProgress, ProgressBar, ProgressState, ProgressStyle,
16};
17use tracing::{field, Level};
18use tracing_core::{span::Id, Event, Field, Subscriber};
19use tracing_subscriber::{
20    filter::{Directive, ParseError},
21    fmt::{
22        self,
23        format::{self, Format},
24        FmtContext, FormatEvent, FormatFields, MakeWriter,
25    },
26    layer::{Context, SubscriberExt},
27    registry::LookupSpan,
28    util::SubscriberInitExt,
29    EnvFilter, Layer,
30};
31
32/// A custom formatter for tracing events.
33pub struct TracingFormatter;
34
35impl<S, N> FormatEvent<S, N> for TracingFormatter
36where
37    S: Subscriber + for<'a> LookupSpan<'a>,
38    N: for<'a> FormatFields<'a> + 'static,
39{
40    fn format_event(
41        &self,
42        ctx: &FmtContext<'_, S, N>,
43        mut writer: format::Writer<'_>,
44        event: &Event<'_>,
45    ) -> std::fmt::Result {
46        let metadata = event.metadata();
47        if *metadata.level() == tracing_core::metadata::Level::INFO
48            && metadata.target().starts_with("rattler_build")
49        {
50            ctx.format_fields(writer.by_ref(), event)?;
51            writeln!(writer)
52        } else {
53            let default_format = Format::default();
54            default_format.format_event(ctx, writer, event)
55        }
56    }
57}
58
59#[derive(Debug, Default)]
60struct SharedState {
61    indentation_level: usize,
62    timestamps: HashMap<Id, Instant>,
63    formatted_spans: HashMap<Id, String>,
64    warnings: Vec<String>,
65}
66
67struct CustomVisitor<'a> {
68    writer: &'a mut dyn io::Write,
69    result: io::Result<()>,
70}
71
72impl<'a> CustomVisitor<'a> {
73    fn new(writer: &'a mut dyn io::Write) -> Self {
74        Self {
75            writer,
76            result: Ok(()),
77        }
78    }
79}
80
81impl<'a> field::Visit for CustomVisitor<'a> {
82    fn record_str(&mut self, field: &Field, value: &str) {
83        if self.result.is_err() {
84            return;
85        }
86
87        self.record_debug(field, &format_args!("{}", value))
88    }
89
90    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
91        if self.result.is_err() {
92            return;
93        }
94
95        self.result = match field.name() {
96            "message" => write!(self.writer, "{:?}", value),
97            "recipe" => write!(self.writer, " recipe: {:?}", value),
98            "package" => write!(self.writer, " package: {:?}", value),
99            _ => Ok(()),
100        };
101    }
102}
103
104fn chunk_string_without_ansi(input: &str, max_chunk_length: usize) -> Vec<String> {
105    let mut chunks: Vec<String> = vec![];
106    let mut current_chunk = String::new();
107    let mut current_length = 0;
108    let mut chars = input.chars().peekable();
109
110    while let Some(c) = chars.next() {
111        if c == '\x1B' {
112            // Beginning of an ANSI escape sequence
113            current_chunk.push(c);
114            while let Some(&next_char) = chars.peek() {
115                // Add to current chunk
116                current_chunk.push(chars.next().unwrap());
117                if next_char.is_ascii_alphabetic() {
118                    // End of ANSI escape sequence
119                    break;
120                }
121            }
122        } else {
123            // Regular character
124            current_length += 1;
125            current_chunk.push(c);
126            if current_length == max_chunk_length {
127                // Current chunk is full
128                chunks.push(current_chunk);
129                current_chunk = String::new();
130                current_length = 0;
131            }
132        }
133    }
134
135    // Add the last chunk if it's not empty
136    if !current_chunk.is_empty() {
137        chunks.push(current_chunk);
138    }
139
140    chunks
141}
142
143fn indent_levels(indent: usize) -> String {
144    let mut s = String::new();
145    for _ in 0..indent {
146        s.push_str(" │");
147    }
148    format!("{}", style(s).cyan())
149}
150
151impl<S> Layer<S> for LoggingOutputHandler
152where
153    S: Subscriber + for<'a> LookupSpan<'a>,
154{
155    fn on_new_span(
156        &self,
157        attrs: &tracing_core::span::Attributes<'_>,
158        id: &tracing_core::span::Id,
159        ctx: Context<'_, S>,
160    ) {
161        let mut state = self.state.lock().unwrap();
162        state.timestamps.insert(id.clone(), Instant::now());
163        let span = ctx.span(id);
164
165        if let Some(span) = span {
166            let mut s = Vec::new();
167            let mut w = io::Cursor::new(&mut s);
168            attrs.record(&mut CustomVisitor::new(&mut w));
169            let s = String::from_utf8_lossy(w.get_ref());
170
171            if !s.is_empty() {
172                state
173                    .formatted_spans
174                    .insert(id.clone(), format!("{}{}", span.name(), s));
175            } else {
176                state
177                    .formatted_spans
178                    .insert(id.clone(), span.name().to_string());
179            }
180        }
181    }
182
183    fn on_enter(&self, id: &Id, _ctx: Context<'_, S>) {
184        let mut state = self.state.lock().unwrap();
185        let ind = indent_levels(state.indentation_level);
186        if let Some(txt) = state.formatted_spans.get(id) {
187            eprintln!("{ind}\n{ind} {} {}", style("╭─").cyan(), txt);
188        }
189
190        state.indentation_level += 1;
191    }
192
193    fn on_exit(&self, id: &Id, _ctx: Context<'_, S>) {
194        let mut state = self.state.lock().unwrap();
195
196        let prev_ind = indent_levels(state.indentation_level);
197
198        if state.indentation_level > 0 {
199            state.indentation_level -= 1;
200        }
201
202        let ind = indent_levels(state.indentation_level);
203
204        let elapsed_time = state
205            .timestamps
206            .remove(id)
207            .map(|t| t.elapsed())
208            .unwrap_or_default();
209
210        let human_duration = HumanDuration(elapsed_time);
211
212        eprintln!(
213            "{prev_ind}\n{ind} {} (took {})",
214            style("╰───────────────────").cyan(),
215            human_duration
216        );
217    }
218
219    fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
220        let mut state = self.state.lock().unwrap();
221        let indent_str = indent_levels(state.indentation_level);
222
223        let mut s = Vec::new();
224        event.record(&mut CustomVisitor::new(&mut s));
225        let s = String::from_utf8_lossy(&s);
226
227        let (prefix, prefix_len) =
228            if event.metadata().level() <= &tracing_core::metadata::Level::WARN {
229                state.warnings.push(s.to_string());
230                if event.metadata().level() == &tracing_core::metadata::Level::ERROR {
231                    (style("× error ").red().bold(), 7)
232                } else {
233                    (style("⚠ warning ").yellow().bold(), 9)
234                }
235            } else {
236                (style(""), 0)
237            };
238
239        let width: usize = terminal_size::terminal_size()
240            .map(|(w, _)| w.0)
241            .unwrap_or(160) as usize;
242
243        let max_width = width - (state.indentation_level * 2) - 1 - prefix_len;
244
245        self.progress_bars.suspend(|| {
246            for line in s.lines() {
247                // split line into max_width chunks
248                if line.len() <= max_width {
249                    eprintln!("{} {}{}", indent_str, prefix, line);
250                } else {
251                    chunk_string_without_ansi(line, max_width)
252                        .iter()
253                        .for_each(|chunk| {
254                            eprintln!("{} {}{}", indent_str, prefix, chunk);
255                        });
256                }
257            }
258        });
259    }
260}
261
262/// A custom output handler for fancy logging.
263#[derive(Debug)]
264pub struct LoggingOutputHandler {
265    state: Arc<Mutex<SharedState>>,
266    progress_bars: MultiProgress,
267    writer: io::Stderr,
268}
269
270impl Clone for LoggingOutputHandler {
271    fn clone(&self) -> Self {
272        Self {
273            state: self.state.clone(),
274            progress_bars: self.progress_bars.clone(),
275            writer: io::stderr(),
276        }
277    }
278}
279
280impl Default for LoggingOutputHandler {
281    /// Creates a new output handler.
282    fn default() -> Self {
283        Self {
284            state: Arc::new(Mutex::new(SharedState::default())),
285            progress_bars: MultiProgress::new(),
286            writer: io::stderr(),
287        }
288    }
289}
290
291impl LoggingOutputHandler {
292    /// Create a new logging handler with the given multi-progress.
293    pub fn from_multi_progress(multi_progress: MultiProgress) -> LoggingOutputHandler {
294        Self {
295            state: Arc::new(Mutex::new(SharedState::default())),
296            progress_bars: multi_progress,
297            writer: io::stderr(),
298        }
299    }
300
301    /// Return a string with the current indentation level (bars added to the
302    /// front of the string).
303    pub fn with_indent_levels(&self, template: &str) -> String {
304        let state = self.state.lock().unwrap();
305        let indent_str = indent_levels(state.indentation_level);
306        format!("{} {}", indent_str, template)
307    }
308
309    /// Return the multi-progress instance.
310    pub fn multi_progress(&self) -> &MultiProgress {
311        &self.progress_bars
312    }
313
314    /// Returns the style to use for a progressbar that is currently in
315    /// progress.
316    pub fn default_bytes_style(&self) -> indicatif::ProgressStyle {
317        let template_str = self.with_indent_levels(
318            "{spinner:.green} {prefix:20!} [{elapsed_precise}] [{bar:40!.bright.yellow/dim.white}] {bytes:>8} @ {smoothed_bytes_per_sec:8}"
319        );
320
321        indicatif::ProgressStyle::default_bar()
322            .template(&template_str)
323            .unwrap()
324            .progress_chars("━━╾─")
325            .with_key(
326                "smoothed_bytes_per_sec",
327                |s: &ProgressState, w: &mut dyn std::fmt::Write| match (
328                    s.pos(),
329                    s.elapsed().as_millis(),
330                ) {
331                    (pos, elapsed_ms) if elapsed_ms > 0 => {
332                        // TODO: log with tracing?
333                        _ = write!(
334                            w,
335                            "{}/s",
336                            HumanBytes((pos as f64 * 1000_f64 / elapsed_ms as f64) as u64)
337                        );
338                    }
339                    _ => {
340                        _ = write!(w, "-");
341                    }
342                },
343            )
344    }
345
346    /// Returns the style to use for a progressbar that is currently in
347    /// progress.
348    pub fn default_progress_style(&self) -> indicatif::ProgressStyle {
349        let template_str = self.with_indent_levels(
350            "{spinner:.green} {prefix:20!} [{elapsed_precise}] [{bar:40!.bright.yellow/dim.white}] {pos:>7}/{len:7}"
351        );
352        indicatif::ProgressStyle::default_bar()
353            .template(&template_str)
354            .unwrap()
355            .progress_chars("━━╾─")
356    }
357
358    /// Returns the style to use for a progressbar that is in Deserializing
359    /// state.
360    pub fn deserializing_progress_style(&self) -> indicatif::ProgressStyle {
361        let template_str =
362            self.with_indent_levels("{spinner:.green} {prefix:20!} [{elapsed_precise}] {wide_msg}");
363        indicatif::ProgressStyle::default_bar()
364            .template(&template_str)
365            .unwrap()
366            .progress_chars("━━╾─")
367    }
368
369    /// Returns the style to use for a progressbar that is finished.
370    pub fn finished_progress_style(&self) -> indicatif::ProgressStyle {
371        let template_str = self.with_indent_levels(&format!(
372            "{} {{prefix:20!}} [{{elapsed_precise}}] {{msg:.bold.green}}",
373            console::style(console::Emoji("✔", " ")).green()
374        ));
375
376        indicatif::ProgressStyle::default_bar()
377            .template(&template_str)
378            .unwrap()
379            .progress_chars("━━╾─")
380    }
381
382    /// Returns the style to use for a progressbar that is in error state.
383    pub fn errored_progress_style(&self) -> indicatif::ProgressStyle {
384        let template_str = self.with_indent_levels(&format!(
385            "{} {{prefix:20!}} [{{elapsed_precise}}] {{msg:.bold.red}}",
386            console::style(console::Emoji("×", " ")).red()
387        ));
388
389        indicatif::ProgressStyle::default_bar()
390            .template(&template_str)
391            .unwrap()
392            .progress_chars("━━╾─")
393    }
394
395    /// Returns the style to use for a progressbar that is indeterminate and
396    /// simply shows a spinner.
397    pub fn long_running_progress_style(&self) -> indicatif::ProgressStyle {
398        let template_str = self.with_indent_levels("{spinner:.green} {msg}");
399        ProgressStyle::with_template(&template_str).unwrap()
400    }
401
402    /// Adds a progress bar to the handler.
403    pub fn add_progress_bar(&self, progress_bar: indicatif::ProgressBar) -> indicatif::ProgressBar {
404        self.progress_bars.add(progress_bar)
405    }
406
407    /// Set progress bars to hidden
408    pub fn set_progress_bars_hidden(&self, hidden: bool) {
409        self.progress_bars.set_draw_target(if hidden {
410            indicatif::ProgressDrawTarget::hidden()
411        } else {
412            indicatif::ProgressDrawTarget::stderr()
413        });
414    }
415
416    /// Displays a spinner with the given message while running the specified
417    /// function to completion.
418    pub fn wrap_in_progress<T, F: FnOnce() -> T>(
419        &self,
420        msg: impl Into<Cow<'static, str>>,
421        func: F,
422    ) -> T {
423        let pb = self.add_progress_bar(
424            ProgressBar::hidden()
425                .with_style(self.long_running_progress_style())
426                .with_message(msg),
427        );
428        pb.enable_steady_tick(Duration::from_millis(100));
429        let result = func();
430        pb.finish_and_clear();
431        result
432    }
433
434    /// Displays a spinner with the given message while running the specified
435    /// function to completion.
436    pub async fn wrap_in_progress_async<T, Fut: Future<Output = T>>(
437        &self,
438        msg: impl Into<Cow<'static, str>>,
439        future: Fut,
440    ) -> T {
441        self.wrap_in_progress_async_with_progress(msg, |_pb| future)
442            .await
443    }
444
445    /// Displays a spinner with the given message while running the specified
446    /// function to completion.
447    pub async fn wrap_in_progress_async_with_progress<
448        T,
449        Fut: Future<Output = T>,
450        F: FnOnce(ProgressBar) -> Fut,
451    >(
452        &self,
453        msg: impl Into<Cow<'static, str>>,
454        f: F,
455    ) -> T {
456        let pb = self.add_progress_bar(
457            ProgressBar::hidden()
458                .with_style(self.long_running_progress_style())
459                .with_message(msg),
460        );
461        pb.enable_steady_tick(Duration::from_millis(100));
462        let result = f(pb.clone()).await;
463        pb.finish_and_clear();
464        result
465    }
466}
467
468impl io::Write for LoggingOutputHandler {
469    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
470        self.progress_bars.suspend(|| self.writer.write(buf))
471    }
472
473    fn flush(&mut self) -> io::Result<()> {
474        self.progress_bars.suspend(|| self.writer.flush())
475    }
476}
477
478impl<'a> MakeWriter<'a> for LoggingOutputHandler {
479    type Writer = LoggingOutputHandler;
480
481    fn make_writer(&'a self) -> Self::Writer {
482        self.clone()
483    }
484}
485///////////////////////
486// LOGGING CLI utils //
487///////////////////////
488
489/// The style to use for logging output.
490#[derive(clap::ValueEnum, Clone, Eq, PartialEq, Debug, Copy)]
491pub enum LogStyle {
492    /// Use fancy logging output.
493    Fancy,
494    /// Use JSON logging output.
495    Json,
496    /// Use plain logging output.
497    Plain,
498}
499
500/// Constructs a default [`EnvFilter`] that is used when the user did not
501/// specify a custom RUST_LOG.
502pub fn get_default_env_filter(
503    verbose: clap_verbosity_flag::LevelFilter,
504) -> Result<EnvFilter, ParseError> {
505    let mut result = EnvFilter::new(format!("rattler_build={verbose}"));
506
507    if verbose >= clap_verbosity_flag::LevelFilter::Trace {
508        result = result.add_directive(Directive::from_str("resolvo=info")?);
509        result = result.add_directive(Directive::from_str("rattler=info")?);
510        result = result.add_directive(Directive::from_str(
511            "rattler_networking::authentication_storage=info",
512        )?);
513    } else {
514        result = result.add_directive(Directive::from_str("resolvo=warn")?);
515        result = result.add_directive(Directive::from_str("rattler=warn")?);
516        result = result.add_directive(Directive::from_str("rattler_repodata_gateway::fetch=off")?);
517        result = result.add_directive(Directive::from_str(
518            "rattler_networking::authentication_storage=off",
519        )?);
520    }
521
522    Ok(result)
523}
524
525struct GitHubActionsLayer(bool);
526
527impl<S: Subscriber> Layer<S> for GitHubActionsLayer {
528    fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
529        if !self.0 {
530            return;
531        }
532        let metadata = event.metadata();
533
534        let mut message = Vec::new();
535        event.record(&mut CustomVisitor::new(&mut message));
536        let message = String::from_utf8_lossy(&message);
537
538        match *metadata.level() {
539            Level::ERROR => println!("::error ::{}", message),
540            Level::WARN => println!("::warning ::{}", message),
541            _ => {}
542        }
543    }
544}
545
546/// Whether to use colors in the output.
547#[derive(clap::ValueEnum, Clone, Eq, PartialEq, Debug, Copy, Default)]
548pub enum Color {
549    /// Always use colors.
550    Always,
551    /// Never use colors.
552    Never,
553    /// Use colors when the output is a terminal.
554    #[default]
555    Auto,
556}
557
558/// Initializes logging with the given style and verbosity.
559pub fn init_logging(
560    log_style: &LogStyle,
561    verbosity: &Verbosity<InfoLevel>,
562    color: &Color,
563    #[cfg(feature = "tui")] tui_log_sender: Option<
564        tokio::sync::mpsc::UnboundedSender<crate::tui::event::Event>,
565    >,
566) -> Result<LoggingOutputHandler, ParseError> {
567    let log_handler = LoggingOutputHandler::default();
568
569    let use_colors = match color {
570        Color::Always => Some(true),
571        Color::Never => Some(false),
572        Color::Auto => None,
573    };
574
575    // Enable disable colors for the colors crate
576    if let Some(use_colors) = use_colors {
577        console::set_colors_enabled(use_colors);
578        console::set_colors_enabled_stderr(use_colors);
579    }
580
581    // Setup tracing subscriber
582    let registry =
583        tracing_subscriber::registry().with(get_default_env_filter(verbosity.log_level_filter())?);
584
585    let log_style = if verbosity.log_level_filter() >= clap_verbosity_flag::LevelFilter::Debug {
586        LogStyle::Plain
587    } else {
588        *log_style
589    };
590
591    let registry = registry.with(GitHubActionsLayer(github_integration_enabled()));
592
593    #[cfg(feature = "tui")]
594    {
595        if let Some(tui_log_sender) = tui_log_sender {
596            log_handler.set_progress_bars_hidden(true);
597            let writer = crate::tui::logger::TuiOutputHandler {
598                log_sender: tui_log_sender,
599            };
600            registry
601                .with(
602                    fmt::layer()
603                        .with_writer(writer)
604                        .without_time()
605                        .with_level(false)
606                        .with_target(false),
607                )
608                .init();
609            return Ok(log_handler);
610        }
611    }
612
613    match log_style {
614        LogStyle::Fancy => {
615            registry.with(log_handler.clone()).init();
616        }
617        LogStyle::Plain => {
618            registry
619                .with(
620                    fmt::layer()
621                        .with_writer(log_handler.clone())
622                        .event_format(TracingFormatter),
623                )
624                .init();
625        }
626        LogStyle::Json => {
627            log_handler.set_progress_bars_hidden(true);
628            registry
629                .with(fmt::layer().json().with_writer(io::stderr))
630                .init();
631        }
632    }
633
634    Ok(log_handler)
635}
636
637/// check if we are on Github CI nad if the user has enabled the integration
638pub fn github_integration_enabled() -> bool {
639    std::env::var("GITHUB_ACTIONS").is_ok()
640        && std::env::var("RATTLER_BUILD_ENABLE_GITHUB_INTEGRATION") == Ok("true".to_string())
641}