Skip to main content

lean_ctx/core/patterns/
make.rs

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