logfmt: teach the make classifier kbuild status tags and warnings

Kernel builds abbreviate every invocation to an uppercase status tag
(CC, LD, HOSTCC, MODPOST, ...) instead of printing the full command
line, so the substring rules that catch classic make output never
fired and the rolling pane went static for the whole kbuild stream.
Recognize the common tags after leading whitespace and show those
lines.

Also surface warnings instead of hiding them: gcc's lowercase
'file.c:12:3: warning:' diagnostics and kbuild's line-leading
'WARNING:' banners (modpost, etc.) now map to Action::Warning.
This commit is contained in:
2026-09-29 16:35:30 +02:00
parent 2de191cf6d
commit 8cf9489072
+96 -8
View File
@@ -5,7 +5,7 @@
//! errors) and whether the line carries countable progress. Classifiers are
//! pure with respect to the UI: they only return [`Action`]s.
use std::sync::OnceLock;
use std::sync::{LazyLock, OnceLock};
use crate::context::Stream;
use regex::Regex;
@@ -226,7 +226,8 @@ impl Classifier for QuiltClassifier {
/// Classifier for make/cmake-based builds (`debian/rules build`, dh helpers)
///
/// Detects `[ 42%]`-style progress markers, hides directory enter/leave
/// noise, and shows compile/link/dh lines.
/// noise, and shows compile/link/dh lines as well as kbuild status-tag
/// lines and warnings.
#[derive(Default)]
pub struct MakeClassifier {}
@@ -242,6 +243,18 @@ impl Classifier for MakeClassifier {
static PERCENT_RE: OnceLock<Regex> = OnceLock::new();
let percent_re = PERCENT_RE.get_or_init(|| Regex::new(r"\[\s*(\d+)%\]").unwrap());
// kbuild abbreviates each invocation to an uppercase status tag
// (` CC [M] drivers/x.o`, ` HOSTCC scripts/foo`) instead of
// printing the full command line, so the classic-make substring
// rules below never fire for kernel builds; this tag list is the
// kbuild counterpart of those substrings.
static KBUILD_TAG_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"^\s*(CC|HOSTCC|HOSTLD|HOSTAR|LD|AR|AS|OBJCOPY|STRIP|GEN|UPD|DESCEND|MODPOST|MODORDER|BTF|SHIPPED|INSTALL|INSTALLMOD|DEPMOD|SIGN|WRAP|SYSHDR|SYSTBL|KSYMS)\b",
)
.unwrap()
});
if let Some(caps) = percent_re.captures(line) {
let pct: u64 = caps[1].parse().unwrap_or(0);
return vec![
@@ -253,12 +266,6 @@ impl Classifier for MakeClassifier {
];
}
if line.contains("make[")
&& (line.contains("Entering directory") || line.contains("Leaving directory"))
{
return vec![Action::Hidden];
}
if line.contains("error:")
|| line.contains("Error ")
|| line.contains("*** [")
@@ -267,6 +274,19 @@ impl Classifier for MakeClassifier {
return vec![Action::Error(truncate(line))];
}
// gcc-style diagnostics (`file.c:12:3: warning: ...`) and kbuild's
// uppercase banners (`WARNING: modpost: ...`) must not be drowned
// out by the activity checks below.
if line.contains("warning:") || line.trim_start().starts_with("WARNING:") {
return vec![Action::Warning(truncate(line))];
}
if line.contains("make[")
&& (line.contains("Entering directory") || line.contains("Leaving directory"))
{
return vec![Action::Hidden];
}
if line.starts_with("dh_")
|| line.contains("gcc ")
|| line.contains("g++ ")
@@ -274,6 +294,7 @@ impl Classifier for MakeClassifier {
|| line.contains("clang")
|| line.contains("ld ")
|| line.contains("ar ")
|| KBUILD_TAG_RE.is_match(line)
{
return vec![Action::Shown(truncate(line))];
}
@@ -517,6 +538,73 @@ mod tests {
);
}
#[test]
fn test_make_shows_kbuild_status_tags() {
let mut c = MakeClassifier::new();
assert_eq!(
feed_one(&mut c, " CC [M] drivers/net/foo.o"),
vec![Action::Shown(" CC [M] drivers/net/foo.o".to_string())]
);
assert_eq!(
feed_one(&mut c, " LD [M] drivers/bar.ko"),
vec![Action::Shown(" LD [M] drivers/bar.ko".to_string())]
);
assert_eq!(
feed_one(&mut c, " HOSTCC scripts/tool"),
vec![Action::Shown(" HOSTCC scripts/tool".to_string())]
);
assert_eq!(
feed_one(&mut c, " MODPOST 1 modules"),
vec![Action::Shown(" MODPOST 1 modules".to_string())]
);
// kbuild tag lines still roll the pane while make progress markers
// update the progress bar
assert_eq!(
feed_one(&mut c, "[ 45%] Building C object x.o"),
vec![
Action::Shown("[ 45%] Building C object x.o".to_string()),
Action::Progress {
pos: 45,
total: 100
}
]
);
// plain noise stays hidden
assert_eq!(
feed_one(&mut c, "test -x debian/scripts/foo"),
vec![Action::Hidden]
);
}
#[test]
fn test_make_shows_warnings_and_keeps_regressions() {
let mut c = MakeClassifier::new();
assert_eq!(
feed_one(&mut c, "drivers/x.c:12:3: warning: unused variable 'y'"),
vec![Action::Warning(
"drivers/x.c:12:3: warning: unused variable 'y'".to_string()
)]
);
assert_eq!(
feed_one(&mut c, "WARNING: modpost: missing MODULE_LICENSE"),
vec![Action::Warning(
"WARNING: modpost: missing MODULE_LICENSE".to_string()
)]
);
// configure-stage probe lines embed the toolchain name and must
// stay visible
assert_eq!(
feed_one(
&mut c,
"checking whether riscv64-linux-gnu-gcc understands -c and -o together... (cached) yes"
),
vec![Action::Shown(
"checking whether riscv64-linux-gnu-gcc understands -c and -o together... (cached) yes"
.to_string()
)]
);
}
#[test]
fn test_mmdebstrap_prefixes() {
let mut c = MmdebstrapClassifier::new();