lean_ctx/core/patterns/
make.rs1use 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}