crates/nornir-build-thing/examples/kconfig_why.rs
//! **The measuring door for the kconfig solver** (lane AP, 2026-08-30).
//!
//! ```text
//! cargo run -p nornir-build-thing --example kconfig_why -- \
//! <linux-src> <fragment.config> <built .config> <base .config> [--list-unexplained]
//! ```
//!
//! Prints the real per-class counts, the ratio they represent, how many derived
//! symbols now carry a chain, and — by name — the ones that do not. It exists
//! so the numbers in a report are read off a run rather than off a belief.
use std::collections::BTreeMap;
use std::path::PathBuf;
use nornir_build_thing::kconfig_graph::{Cause, KconfigGraph, Solver, Unexplained, kconfig_env};
use nornir_build_thing::kernel_requires::{KernelRequirements, WhyIndex};
fn main() -> anyhow::Result<()> {
let a: Vec<String> = std::env::args().skip(1).collect();
if a.len() < 4 {
eprintln!("usage: kconfig_why <linux-src> <fragment> <built.config> <base.config> [--list-unexplained] [--chains N] [--heads]");
std::process::exit(2);
}
let src = PathBuf::from(&a[0]);
let fragment = std::fs::read_to_string(&a[1])?;
let built = std::fs::read_to_string(&a[2])?;
let base = std::fs::read_to_string(&a[3])?;
let list = a.iter().any(|x| x == "--list-unexplained");
let heads = a.iter().any(|x| x == "--heads");
let dump = a.iter().any(|x| x == "--dump");
let chains: usize = a
.windows(2)
.find(|w| w[0] == "--chains")
.and_then(|w| w[1].parse().ok())
.unwrap_or(0);
let t0 = std::time::Instant::now();
let g = KconfigGraph::read(&src, kconfig_env("x86_64", "6.12.104"))?;
let read_ms = t0.elapsed().as_millis();
eprintln!(
"graph: {} Kconfig files, {} symbols, {} select edges, {} imply edges, {read_ms} ms",
g.files_read.len(),
g.symbols.len(),
g.selected_by.values().map(Vec::len).sum::<usize>(),
g.implied_by.values().map(Vec::len).sum::<usize>(),
);
for d in g.dangling_declarations(&fragment) {
println!("DANGLING {}", d.refusal());
}
let s = Solver::new(&g, &fragment, &built, &base);
let derived = s.derived();
let live = s.config.values.values().filter(|v| v.as_str() != "n").count();
println!("live symbols in the built .config : {live}");
println!("named by the merged fragment : {}", s.declared.len());
println!(
"kconfig-derived : {} ({:.1} % of live)",
derived.len(),
derived.len() as f64 * 100.0 / live as f64
);
let t1 = std::time::Instant::now();
let all = s.explain_all();
let solve_ms = t1.elapsed().as_millis();
let mut by_head_class: BTreeMap<&str, usize> = BTreeMap::new();
let mut unex: BTreeMap<String, Unexplained> = BTreeMap::new();
let mut declared_head_counts: BTreeMap<String, usize> = BTreeMap::new();
let mut shown = 0usize;
for (sym, c) in &all {
match c {
Cause::Explained { chain, heads } => {
*by_head_class.entry(chain.head.class()).or_default() += 1;
for h in heads {
if h.class() == "declared" {
*declared_head_counts.entry(h.name().to_string()).or_default() += 1;
}
}
if shown < chains {
println!(" {}", chain.render());
shown += 1;
}
}
Cause::Unexplained(u) => {
unex.insert(sym.clone(), u.clone());
}
}
}
println!("explained : {} ({:.1} %) in {solve_ms} ms", all.len() - unex.len(), (all.len() - unex.len()) as f64 * 100.0 / all.len() as f64);
for (k, v) in &by_head_class {
println!(" head = {k:<16} {v}");
}
println!("UNEXPLAINED : {} ({:.1} %)", unex.len(), unex.len() as f64 * 100.0 / all.len() as f64);
let mut kinds: BTreeMap<&str, usize> = BTreeMap::new();
for u in unex.values() {
*kinds
.entry(match u {
Unexplained::NoDeclaration => "no-config-block",
Unexplained::NoLiveCause => "no-live-cause",
Unexplained::OpaqueCause(_) => "opaque-cause",
})
.or_default() += 1;
}
for (k, v) in &kinds {
println!(" {k:<20} {v}");
}
if list {
for (s, u) in &unex {
println!("UNEXPLAINED CONFIG_{s} : {u:?}");
}
}
if dump {
// `SYMBOL \t class:name,class:name` — one line per derived symbol, every
// head that can reach it. This is what the INVERSE test reads: it must
// be able to ask "is H the ONLY head of S", not merely "is H one of them".
for (sym, c) in &all {
match c {
Cause::Explained { chain, heads } => {
let hs: Vec<String> =
heads.iter().map(|h| format!("{}:{}", h.class(), h.name())).collect();
println!("DUMP\t{sym}\t{}\t{}", hs.join(","), chain.render());
}
Cause::Unexplained(u) => println!("DUMP\t{sym}\t!unexplained\t{u:?}"),
}
}
let gates = s.gate_closure();
for (sym, hs) in &gates {
let v: Vec<String> = hs.iter().map(|h| format!("{}:{}", h.class(), h.name())).collect();
println!("GATE\t{sym}\t{}", v.join(","));
}
}
if let Some(w) = a.windows(2).find(|w| w[0] == "--emit-why").map(|w| w[1].clone()) {
// The FILE an operator reads, through the crate's own emitter — not a
// second rendering of the same data (LAW 5).
let reqs = KernelRequirements::default();
let why = WhyIndex::of(&fragment, &reqs)
.with_built_config(&built, &reqs)
.with_kconfig_chains(&src, &fragment, &built, &base, "x86_64", "6.12.104")?;
std::fs::write(&w, why.to_toml())?;
eprintln!("wrote {w}; unexplained derived rows: {}", why.unexplained_derived().len());
}
if heads {
let mut v: Vec<_> = declared_head_counts.into_iter().collect();
v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
for (h, n) in v {
println!("HEAD CONFIG_{h} reaches {n} derived symbols");
}
}
Ok(())
}