use rpm_spec::ast::{Span, SpecFile, Tag};
use crate::diagnostic::{Diagnostic, LintCategory, Severity};
use crate::files::{FilesClassifier, for_each_files_entry};
use crate::lint::{Lint, LintMetadata};
use crate::rules::util::collect_top_level_dep_names;
use crate::visit::Visit;
use rpm_spec_profile::Profile;
pub static METADATA: LintMetadata = LintMetadata {
id: "RPM325",
name: "pkgconfig-file-without-pkgconfig-br",
description: "`%files` ships a `.pc` file but `BuildRequires:` lacks `pkgconfig`. Without \
the BR, rpm's `pkgconfig(...)` provides generator does not run; downstream \
`-devel` consumers can't find the capability.",
default_severity: Severity::Warn,
category: LintCategory::Packaging,
};
#[derive(Debug, Default)]
pub struct PkgconfigFileWithoutPkgconfigBr {
diagnostics: Vec<Diagnostic>,
profile: Profile,
enabled: bool,
}
impl PkgconfigFileWithoutPkgconfigBr {
pub fn new() -> Self {
Self::default()
}
}
fn family_applies(profile: &Profile) -> bool {
profile
.identity
.family
.is_some_and(rpm_spec_profile::Family::has_offline_build_chroot)
}
impl<'ast> Visit<'ast> for PkgconfigFileWithoutPkgconfigBr {
fn visit_spec(&mut self, spec: &'ast SpecFile<Span>) {
if !self.enabled {
return;
}
let classifier = FilesClassifier::new(&self.profile);
let mut first_pc_span: Option<Span> = None;
for_each_files_entry(spec, |entry| {
if first_pc_span.is_some() {
return;
}
let cls = classifier.classify(entry);
if cls.kind_hints.is_pkgconfig {
first_pc_span = Some(cls.span());
}
});
let Some(span) = first_pc_span else {
return;
};
let brs = collect_top_level_dep_names(spec, |t| matches!(t, Tag::BuildRequires));
if brs.contains("pkgconfig") {
return;
}
self.diagnostics.push(Diagnostic::new(
&METADATA,
Severity::Warn,
"package ships a `.pc` file but `BuildRequires:` lacks `pkgconfig` — the \
`pkgconfig(...)` provides generator won't run",
span,
));
}
}
impl Lint for PkgconfigFileWithoutPkgconfigBr {
fn metadata(&self) -> &'static LintMetadata {
&METADATA
}
fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
std::mem::take(&mut self.diagnostics)
}
fn applies_to_profile(&self, profile: &Profile) -> bool {
family_applies(profile)
}
fn set_profile(&mut self, profile: &Profile) {
self.enabled = family_applies(profile);
self.profile = profile.clone();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::parse;
use rpm_spec_profile::{Family, MacroEntry, Profile, Provenance};
fn fedora_profile() -> Profile {
let mut p = Profile::default();
p.identity.family = Some(Family::Fedora);
for (n, b) in [("_prefix", "/usr"), ("_libdir", "/usr/lib64")] {
p.macros
.insert(n, MacroEntry::literal(b, Provenance::Override));
}
p
}
fn run(src: &str) -> Vec<Diagnostic> {
let outcome = parse(src);
let mut lint = PkgconfigFileWithoutPkgconfigBr::new();
lint.set_profile(&fedora_profile());
lint.visit_spec(&outcome.spec);
lint.take_diagnostics()
}
#[test]
fn flags_pc_without_pkgconfig_br() {
let src = "Name: x\n%files\n/usr/lib64/pkgconfig/foo.pc\n";
let diags = run(src);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].lint_id, "RPM325");
}
#[test]
fn silent_with_pkgconfig_br() {
let src = "Name: x\nBuildRequires: pkgconfig\n%files\n/usr/lib64/pkgconfig/foo.pc\n";
assert!(run(src).is_empty());
}
#[test]
fn silent_without_pc_file() {
let src = "Name: x\n%files\n/usr/bin/foo\n";
assert!(run(src).is_empty());
}
#[test]
fn one_diagnostic_for_multiple_pc_files() {
let src = "Name: x\n%files\n/usr/lib64/pkgconfig/foo.pc\n/usr/lib64/pkgconfig/bar.pc\n";
assert_eq!(run(src).len(), 1);
}
#[test]
fn silent_on_generic_profile() {
let outcome = parse("Name: x\n%files\n/usr/lib64/pkgconfig/foo.pc\n");
let mut lint = PkgconfigFileWithoutPkgconfigBr::new();
lint.set_profile(&Profile::default());
lint.visit_spec(&outcome.spec);
assert!(lint.take_diagnostics().is_empty());
}
}