Skip to main content

lean_ctx/core/patterns/
make.rs

1use regex::Regex;
2use std::collections::HashMap;
3use std::sync::OnceLock;
4
5static COMPILER_INVOCATION_RE: OnceLock<Regex> = OnceLock::new();
6
7fn compiler_invocation_re() -> &'static Regex {
8    COMPILER_INVOCATION_RE.get_or_init(|| {
9        Regex::new(
10            r"(?i)^(/[^\s]+/)?(gcc|g\+\+|c\+\+|cc|clang\+\+|clang|ld\.bfd|ld\.gold|ld|ar|rustc|javac|zig|nvcc|emcc|icc|icpc)\s",
11        )
12        .unwrap()
13    })
14}
15
16fn is_compiler_echo_line(line: &str) -> bool {
17    compiler_invocation_re().is_match(line.trim_start())
18}
19
20fn is_warning_line(line: &str) -> bool {
21    let l = line.to_ascii_lowercase();
22    l.contains("warning:") || l.contains(" warning ") || l.contains(": warning ")
23}
24
25fn is_error_line(line: &str) -> bool {
26    let l = line.to_ascii_lowercase();
27    l.contains("error:")
28        || l.contains("error ")
29        || l.contains("*** ")
30        || l.contains("fatal error")
31        || l.contains("undefined reference")
32        || l.contains("undefined symbol")
33        || l.contains("make: ***")
34        || l.contains("gmake: ***")
35        || l.contains("ninja: error")
36}
37
38fn is_make_meta_line(line: &str) -> bool {
39    let t = line.trim_start();
40    t.starts_with("make[") || t.starts_with("gmake[") || t.starts_with("make:")
41}
42
43pub fn compress(command: &str, output: &str) -> Option<String> {
44    let cl = command.trim();
45    let cl = cl.to_ascii_lowercase();
46    if cl != "make" && !cl.starts_with("make ") {
47        return None;
48    }
49    Some(compress_make_output(output))
50}
51
52fn compress_make_output(output: &str) -> String {
53    let mut kept_non_warning = Vec::new();
54    let mut warning_counts: HashMap<String, u32> = HashMap::new();
55    let mut last_significant: Option<String> = None;
56
57    for line in output.lines() {
58        let trimmed = line.trim_end();
59        if trimmed.is_empty() {
60            continue;
61        }
62        if is_compiler_echo_line(trimmed) {
63            continue;
64        }
65
66        last_significant = Some(trimmed.to_string());
67
68        if is_warning_line(trimmed) {
69            let key = trimmed.trim().to_string();
70            *warning_counts.entry(key).or_insert(0) += 1;
71            continue;
72        }
73
74        let tl = trimmed.to_ascii_lowercase();
75        if tl.contains("nothing to be done")
76            || tl.contains("is up to date.")
77            || is_error_line(trimmed)
78            || is_make_meta_line(trimmed)
79        {
80            kept_non_warning.push(trimmed.to_string());
81            continue;
82        }
83
84        if trimmed.starts_with('@') {
85            continue;
86        }
87
88        if trimmed.contains("Entering directory") || trimmed.contains("Leaving directory") {
89            kept_non_warning.push(trimmed.to_string());
90        }
91    }
92
93    if let Some(ref last) = last_significant {
94        if !kept_non_warning.iter().any(|k| k == last) {
95            kept_non_warning.push(format!("result: {last}"));
96        }
97    }
98
99    let mut sections: Vec<String> = Vec::new();
100
101    if !warning_counts.is_empty() {
102        let mut wlines: Vec<String> = warning_counts
103            .into_iter()
104            .map(|(text, n)| {
105                if n > 1 {
106                    format!("{text} (x{n})")
107                } else {
108                    text
109                }
110            })
111            .collect();
112        wlines.sort();
113        sections.push(format!("warnings:\n{}", wlines.join("\n")));
114    }
115
116    if !kept_non_warning.is_empty() {
117        sections.push(kept_non_warning.join("\n"));
118    }
119
120    if sections.is_empty() {
121        "make (no warnings/errors/meta)".to_string()
122    } else {
123        sections.join("\n\n")
124    }
125}