The specs describe shipped features — incremental builds, the live view, overlayfs staging, pkh new, pkh test, pkh lint phase 1 — and the satellite-tools plan is implemented in full (no shell-outs to dpkg-architecture, dpkg-checkbuilddeps or dpkg-parsechangelog remain). The two unshipped proposals (the lint porting track and the data/configuration architecture) stay in whatever notes the project keeps outside the tree; the README remains the user-facing documentation.
187 lines
6.2 KiB
Rust
187 lines
6.2 KiB
Rust
//! `pkh lint`: lint a Debian source tree, with lintian feature parity.
|
|
//!
|
|
//! The strategy is *wrap first, port second*: the
|
|
//! [wrapper](wrapper) runs the installed lintian over an ephemeral source
|
|
//! package for day-one parity with every lintian check, while the native
|
|
//! engine (check registry, collectors, emitter) hosts pkh-specific workflow
|
|
//! checks (`pkh-*` tags) and grows ported lintian checks incrementally.
|
|
//! Both findings merge into one report — deduplicated by tag name, which is
|
|
//! why native checks mirror lintian's tag names for equivalent checks —
|
|
//! rendered as lintian-shaped text or JSON, with lintian's exit-code
|
|
//! contract (0 clean, 1 findings at/above `--fail-on`, 2 runtime error).
|
|
|
|
pub mod check;
|
|
pub mod checks;
|
|
pub mod collect;
|
|
pub mod emit;
|
|
pub mod output;
|
|
pub mod tag;
|
|
pub mod wrapper;
|
|
|
|
use std::collections::HashSet;
|
|
use std::path::PathBuf;
|
|
|
|
use crate::lint::check::Check;
|
|
use crate::lint::collect::LintData;
|
|
use crate::lint::emit::{Finding, LintReport};
|
|
|
|
/// Knobs of one `pkh lint` run, built from the CLI in `main.rs`.
|
|
pub struct LintOptions {
|
|
/// Source tree to lint.
|
|
pub path: PathBuf,
|
|
/// Run the native engine only; never invoke the lintian wrapper.
|
|
pub native: bool,
|
|
/// Levels that make the exit code 1 (`--fail-on`, default: error).
|
|
pub fail_on: Vec<output::Level>,
|
|
/// Show tag explanations under each finding (`--info`, lintian's `-i`).
|
|
pub info: bool,
|
|
/// Also display info-level (`I:`) findings (lintian's `-I`).
|
|
pub display_info: bool,
|
|
/// Also display pedantic (`P:`) findings.
|
|
pub pedantic: bool,
|
|
/// Also display experimental (`X:`) findings.
|
|
pub experimental: bool,
|
|
/// Also display overridden (`O:`) findings.
|
|
pub show_overrides: bool,
|
|
/// Tag names to ignore for this run.
|
|
pub suppress_tags: Vec<String>,
|
|
/// Run only these native checks (`--check`, repeatable).
|
|
pub only_checks: Vec<String>,
|
|
/// Ignore any existing `pkh build` output and pack the tree fresh.
|
|
pub repack: bool,
|
|
/// Emit JSON instead of text.
|
|
pub json: bool,
|
|
/// Text colorization mode (`--color`, default auto: TTY without NO_COLOR).
|
|
pub color: output::ColorMode,
|
|
/// Target distribution (debian/ubuntu), when known.
|
|
pub dist: Option<String>,
|
|
/// Target series, when known.
|
|
pub series: Option<String>,
|
|
}
|
|
|
|
/// Run one lint: collect package information, run the native checks, wrap
|
|
/// lintian (unless `--native`), and merge everything into one report.
|
|
pub fn run(options: &LintOptions) -> Result<LintReport, String> {
|
|
let data = LintData::collect(
|
|
&options.path,
|
|
options.dist.as_deref(),
|
|
options.series.as_deref(),
|
|
);
|
|
let mut report = LintReport {
|
|
findings: Vec::new(),
|
|
source_name: data.source_name.clone(),
|
|
wrapper_unavailable: false,
|
|
notes: Vec::new(),
|
|
};
|
|
|
|
if !options.native {
|
|
match wrapper::run(&data.root, options)? {
|
|
Some(outcome) => {
|
|
report.findings.extend(outcome.findings);
|
|
report.notes = outcome.notes;
|
|
}
|
|
None => report.wrapper_unavailable = true,
|
|
}
|
|
}
|
|
|
|
let selected: Vec<&'static dyn Check> = if options.only_checks.is_empty() {
|
|
check::CHECKS.to_vec()
|
|
} else {
|
|
options
|
|
.only_checks
|
|
.iter()
|
|
.map(|id| check::find_check(id).ok_or_else(|| format!("Unknown --check '{id}'")))
|
|
.collect::<Result<_, _>>()?
|
|
};
|
|
|
|
let mut native = Vec::new();
|
|
{
|
|
let mut emitter = emit::Emitter::new(report.source_name.clone(), &mut native);
|
|
for check in selected {
|
|
check.run(&data, &mut emitter);
|
|
}
|
|
}
|
|
report.findings.extend(merge(&report.findings, native));
|
|
|
|
if !options.suppress_tags.is_empty() {
|
|
report
|
|
.findings
|
|
.retain(|finding| !options.suppress_tags.contains(&finding.tag_name));
|
|
}
|
|
Ok(report)
|
|
}
|
|
|
|
/// Append `native` findings to `lintian`'s, dropping native duplicates by
|
|
/// tag name (the wrapper's verdict wins for tags both engines produce).
|
|
fn merge(lintian: &[Finding], native: Vec<Finding>) -> Vec<Finding> {
|
|
let known: HashSet<&str> = lintian.iter().map(|f| f.tag_name.as_str()).collect();
|
|
native
|
|
.into_iter()
|
|
.filter(|finding| !known.contains(finding.tag_name.as_str()))
|
|
.collect()
|
|
}
|
|
|
|
/// The `--list-tags` catalog: one line per registered native tag.
|
|
pub fn list_tags() -> String {
|
|
let mut out = String::new();
|
|
for check in check::CHECKS {
|
|
for tag in check.tags() {
|
|
out.push_str(&format!(
|
|
"{} [{}] {}\n {}\n",
|
|
tag.letter(),
|
|
check.id(),
|
|
tag.name,
|
|
tag.description
|
|
));
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::lint::emit::Origin;
|
|
|
|
fn finding(tag: &str, origin: Origin) -> Finding {
|
|
Finding {
|
|
letter: 'W',
|
|
tag_name: tag.to_string(),
|
|
message: String::new(),
|
|
package: "hello".to_string(),
|
|
processable_type: Some("source".to_string()),
|
|
explanation: Vec::new(),
|
|
origin,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn merge_drops_native_duplicates_by_tag_name() {
|
|
let lintian = vec![finding("no-debian-copyright-in-source", Origin::Lintian)];
|
|
let native = vec![
|
|
finding("no-debian-copyright-in-source", Origin::Native),
|
|
finding("pkh-debian-changes-not-committed", Origin::Native),
|
|
];
|
|
let merged = merge(&lintian, native);
|
|
assert_eq!(merged.len(), 1);
|
|
assert_eq!(merged[0].tag_name, "pkh-debian-changes-not-committed");
|
|
assert_eq!(merged[0].origin, Origin::Native);
|
|
}
|
|
|
|
#[test]
|
|
fn native_tags_resolve_from_the_registry() {
|
|
let tag = check::find_tag("pkh-debian-changes-not-committed")
|
|
.expect("pkh check tags must resolve");
|
|
assert_eq!(tag.letter(), 'W');
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_native_tags_still_produce_findings() {
|
|
let mut sink = Vec::new();
|
|
let mut emitter = emit::Emitter::new("hello".to_string(), &mut sink);
|
|
emitter.tag("no-such-tag-anywhere", "boom");
|
|
assert_eq!(sink.len(), 1);
|
|
assert_eq!(sink[0].tag_name, "no-such-tag-anywhere");
|
|
}
|
|
}
|