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