Skip to main content

lean_ctx_patterns/
make.rs

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