use std::collections::{BTreeMap, BTreeSet};
use serde::Serialize;
use crate::deps::DepsGraph;
#[derive(Debug, Default, Serialize)]
pub struct DependentsReport {
pub leaves: Vec<Leaf>,
pub carried: Vec<Carried>,
pub externally_needed: Vec<Carried>,
pub unknown: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct Leaf {
pub name: String,
pub binaries: Vec<String>,
pub devel_only: bool,
}
#[derive(Debug, Serialize)]
pub struct Carried {
pub name: String,
pub dependents: Vec<String>,
pub binaries: Vec<String>,
pub devel_only: bool,
}
pub fn classify(graph: &DepsGraph, names: &BTreeSet<String>) -> DependentsReport {
let dependents = graph.dependents();
let mut binaries: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for (binary, source) in &graph.binary_sources {
if !binary.starts_with("src:") {
binaries.entry(source).or_default().push(binary);
}
}
let mut report = DependentsReport::default();
for name in names {
let known = binaries.contains_key(name.as_str())
|| graph.binary_sources.contains_key(&format!("src:{name}"))
|| graph.roots.contains(name);
if !known {
report.unknown.push(name.clone());
continue;
}
let deps = dependents.get(name).cloned().unwrap_or_default();
let bins: Vec<String> = binaries
.get(name.as_str())
.into_iter()
.flatten()
.map(|b| b.to_string())
.collect();
let devel_only = !bins.is_empty() && bins.iter().all(|b| b.ends_with("-devel"));
if deps.is_empty() {
report.leaves.push(Leaf {
devel_only,
name: name.clone(),
binaries: bins,
});
continue;
}
let in_set: Vec<String> = deps
.iter()
.filter(|d| names.contains(*d))
.cloned()
.collect();
match in_set.is_empty() {
false => report.carried.push(Carried {
name: name.clone(),
dependents: in_set,
binaries: bins,
devel_only,
}),
true => report.externally_needed.push(Carried {
name: name.clone(),
dependents: deps.into_iter().collect(),
binaries: bins,
devel_only,
}),
}
}
report
}
fn carried_line(c: &Carried) -> String {
format!(
" {}{} — needed by {}\n",
c.name,
if c.devel_only { " [devel-only]" } else { "" },
c.dependents.join(", ")
)
}
pub fn format_report(report: &DependentsReport) -> String {
let mut out = String::new();
out.push_str(&format!(
"leaves — nothing in the closure needs them ({}):\n",
report.leaves.len()
));
for leaf in &report.leaves {
out.push_str(&format!(
" {}{} — ships {}\n",
leaf.name,
if leaf.devel_only { " [devel-only]" } else { "" },
match leaf.binaries.is_empty() {
true => "nothing the walk saw".to_string(),
false => leaf.binaries.join(", "),
}
));
}
out.push_str(&format!(
"carried — other given packages need them, the walk would \
re-derive them ({}):\n",
report.carried.len()
));
for c in &report.carried {
out.push_str(&carried_line(c));
}
if !report.externally_needed.is_empty() {
out.push_str(&format!(
"externally needed — only closure packages outside the given \
inventories require them ({}):\n",
report.externally_needed.len()
));
for c in &report.externally_needed {
out.push_str(&carried_line(c));
}
}
if !report.unknown.is_empty() {
out.push_str(&format!(
"not in the graph ({}): {} — regenerate with deps --graph?\n",
report.unknown.len(),
report.unknown.join(", ")
));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::deps::GraphProvider;
fn graph() -> DepsGraph {
let mut g = DepsGraph {
roots: vec!["pandoc".into(), "rust-quiet".into(), "rust-anyhow".into()],
..Default::default()
};
for (b, s) in [
("pandoc", "pandoc"),
("rust-quiet-devel", "rust-quiet"),
("rust-anyhow-devel", "rust-anyhow"),
("rust-syn-devel", "rust-syn"),
("rust-quote-devel", "rust-quote"),
] {
g.binary_sources.insert(b.into(), s.into());
}
let edge = |g: &mut DepsGraph, cap: &str, requirer: &str, prov: GraphProvider| {
g.requirers
.entry(cap.into())
.or_default()
.insert(requirer.into());
g.providers.entry(cap.into()).or_default().insert(prov);
};
edge(
&mut g,
"crate(anyhow)",
"pandoc",
GraphProvider {
binary: "rust-anyhow-devel".into(),
source: "rust-anyhow".into(),
repoid: "rawhide".into(),
},
);
edge(
&mut g,
"crate(syn)",
"rust-quote-devel",
GraphProvider {
binary: "rust-syn-devel".into(),
source: "rust-syn".into(),
repoid: "rawhide".into(),
},
);
g
}
#[test]
fn leaves_carried_and_external_split_correctly() {
let names: BTreeSet<String> = ["pandoc", "rust-quiet", "rust-anyhow", "rust-syn"]
.map(String::from)
.into();
let report = classify(&graph(), &names);
let leaves: Vec<(&str, bool)> = report
.leaves
.iter()
.map(|l| (l.name.as_str(), l.devel_only))
.collect();
assert_eq!(leaves, [("pandoc", false), ("rust-quiet", true)]);
assert_eq!(report.carried.len(), 1);
assert_eq!(report.carried[0].name, "rust-anyhow");
assert_eq!(report.carried[0].dependents, ["pandoc"]);
assert!(report.carried[0].devel_only);
assert!(format_report(&report).contains("rust-anyhow [devel-only] — needed by pandoc"));
assert_eq!(report.externally_needed.len(), 1);
assert_eq!(report.externally_needed[0].name, "rust-syn");
assert_eq!(report.externally_needed[0].dependents, ["rust-quote"]);
}
#[test]
fn a_package_the_graph_never_saw_is_unknown_not_a_leaf() {
let names: BTreeSet<String> = ["brand-new".to_string()].into();
let report = classify(&graph(), &names);
assert!(report.leaves.is_empty());
assert_eq!(report.unknown, ["brand-new"]);
assert!(format_report(&report).contains("regenerate with deps --graph?"));
}
#[test]
fn build_time_dependents_count_too() {
let mut g = graph();
g.binary_sources
.insert("src:pandoc".to_string(), "pandoc".to_string());
g.requirers
.get_mut("crate(syn)")
.unwrap()
.insert("src:pandoc".to_string());
let names: BTreeSet<String> = ["pandoc", "rust-syn"].map(String::from).into();
let report = classify(&g, &names);
assert_eq!(report.carried[0].name, "rust-syn");
assert_eq!(report.carried[0].dependents, ["pandoc"]);
}
}