lint: add pkh lint, wrapping lintian for parity plus pkh-native checks
pkh covered the package lifecycle but never validated the packaging itself: broken control stanzas, unparsable changelog versions or uncommitted debian/ edits only surfaced at build or upload time. pkh lint lints a source tree with day-one lintian parity plus a native Rust engine for the checks lintian cannot have. The wrapper reuses the pkh build output next to the tree when it matches the current changelog entry and no tree content is newer (mtime walk, skipping .git/.pc), else packs fresh with dpkg-source -b using weak gzip compression (the artifact is ephemeral; xz dominated the run at 9.8 s versus 2.7 s on a 111 MB tree) and symlinks quilt orig tarballs from the tree's parent, which dpkg-source searches in cwd. Findings are parsed from the installed lintian into a unified report, deduplicated by tag name against the native engine, and rendered lintian-shaped (<L>: <pkg> <type>: <tag> <details>) as text or JSON, colorized at render time (--color auto/always/never). Exit codes follow lintian's contract (0 clean, 1 findings at/above --fail-on, 2 runtime error); lintian's own exit code is ignored because it uses 2 both for findings and for runtime errors. -d/--dist maps to lintian --profile so the target distro's rules apply even on a foreign host. The native engine hosts the first workflow check lintian cannot know: pkh-debian-changes-not-committed flags debian/ content that is not committed to git, since the pkh flow builds and uploads the tree as-is. Checks register in a static registry validated by a unit test, and the wrapper's parser is pinned by golden tests captured from lintian 2.129 output. Strategies for lintian's Ubuntu blind spots (its vendor data there is one file plus 14 disabled tags) are specced in plans/pkh-lint.md, deliberately not implemented yet.
This commit is contained in:
+128
@@ -217,6 +217,63 @@ fn main() {
|
||||
.arg(arg!(--verbose "Show raw tool output instead of the live build view").required(false)
|
||||
.long_help("Show raw tool output instead of the live build view.\nAlso implied by RUST_LOG=debug for pkh's own logs.")),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("lint")
|
||||
.about("Lint the package (lintian wrapper + pkh-native checks)")
|
||||
.arg(arg!([path] "Source tree to lint (default: the current directory)").required(false))
|
||||
.arg(arg!(-d --dist <dist> "Target distribution (debian, ubuntu)").required(false))
|
||||
.arg(arg!(-s --series <series> "Target distribution series").required(false))
|
||||
.arg(arg!(--native "Run pkh-native checks only, without the lintian wrapper").required(false))
|
||||
.arg(arg!(--info "Show tag explanations under each finding").required(false))
|
||||
.arg(
|
||||
clap::Arg::new("display_info")
|
||||
.long("display-info")
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
.help("Also display info-level tags (I:)"),
|
||||
)
|
||||
.arg(arg!(--pedantic "Also display pedantic tags (P:)").required(false))
|
||||
.arg(arg!(--experimental "Also display experimental tags (X:)").required(false))
|
||||
.arg(
|
||||
clap::Arg::new("show_overrides")
|
||||
.long("show-overrides")
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
.help("Also display overridden tags (O:)"),
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("fail_on")
|
||||
.long("fail-on")
|
||||
.value_name("LEVELS")
|
||||
.help("Comma-separated severities failing the run: error, warning, info, pedantic, experimental, override (default: error)"),
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("suppress_tags")
|
||||
.long("suppress-tags")
|
||||
.value_name("LIST")
|
||||
.help("Comma-separated tag names to ignore for this run"),
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("check")
|
||||
.long("check")
|
||||
.value_name("NAME")
|
||||
.action(clap::ArgAction::Append)
|
||||
.help("Run only this pkh-native check (can be specified multiple times)"),
|
||||
)
|
||||
.arg(arg!(--repack "Ignore existing pkh build output and pack the tree fresh for linting").required(false))
|
||||
.arg(arg!(--json "Emit the report as JSON").required(false))
|
||||
.arg(
|
||||
clap::Arg::new("color")
|
||||
.long("color")
|
||||
.value_name("WHEN")
|
||||
.value_parser(["auto", "always", "never"])
|
||||
.help("Colorize the report: auto, always or never (default: auto)"),
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("list_tags")
|
||||
.long("list-tags")
|
||||
.action(clap::ArgAction::SetTrue)
|
||||
.help("Print the pkh-native tag catalog and exit"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("context")
|
||||
.about("Manage contexts")
|
||||
@@ -785,6 +842,77 @@ fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(("lint", sub_matches)) => {
|
||||
if sub_matches.get_flag("list_tags") {
|
||||
print!("{}", pkh::lint::list_tags());
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let path = sub_matches
|
||||
.get_one::<String>("path")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(current_dir_or_exit);
|
||||
let fail_on = match pkh::lint::output::parse_fail_on(
|
||||
sub_matches
|
||||
.get_one::<String>("fail_on")
|
||||
.map(String::as_str)
|
||||
.unwrap_or("error"),
|
||||
) {
|
||||
Ok(levels) => levels,
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
std::process::exit(2);
|
||||
}
|
||||
};
|
||||
let options = pkh::lint::LintOptions {
|
||||
path,
|
||||
native: sub_matches.get_flag("native"),
|
||||
fail_on,
|
||||
info: sub_matches.get_flag("info"),
|
||||
display_info: sub_matches.get_flag("display_info"),
|
||||
pedantic: sub_matches.get_flag("pedantic"),
|
||||
experimental: sub_matches.get_flag("experimental"),
|
||||
show_overrides: sub_matches.get_flag("show_overrides"),
|
||||
suppress_tags: sub_matches
|
||||
.get_one::<String>("suppress_tags")
|
||||
.map(|list| {
|
||||
list.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|tag| !tag.is_empty())
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
only_checks: sub_matches
|
||||
.get_many::<String>("check")
|
||||
.map(|values| values.cloned().collect())
|
||||
.unwrap_or_default(),
|
||||
repack: sub_matches.get_flag("repack"),
|
||||
json: sub_matches.get_flag("json"),
|
||||
color: match sub_matches.get_one::<String>("color").map(String::as_str) {
|
||||
Some("always") => pkh::lint::output::ColorMode::Always,
|
||||
Some("never") => pkh::lint::output::ColorMode::Never,
|
||||
_ => pkh::lint::output::ColorMode::Auto,
|
||||
},
|
||||
dist: sub_matches.get_one::<String>("dist").cloned(),
|
||||
series: sub_matches.get_one::<String>("series").cloned(),
|
||||
};
|
||||
|
||||
match pkh::lint::run(&options) {
|
||||
Ok(report) => {
|
||||
if options.json {
|
||||
println!("{}", pkh::lint::output::render_json(&report, &options));
|
||||
} else {
|
||||
print!("{}", pkh::lint::output::render_text(&report, &options));
|
||||
}
|
||||
std::process::exit(pkh::lint::output::exit_code(&report, &options));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user