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