//! `hammer kernel` — el armador de kernel (SDD 22). //! //! Superficie en inglés (Regla 7.bis), mensajes en castellano. El contrato con las UIs es JSON //! estable, mismo patrón que `/run/hammer/boot-graph.json`. use std::collections::{BTreeMap, BTreeSet}; use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use clap::Subcommand; use hammer_core::kernel::kconfig::KconfigTree; use hammer_core::kernel::KernelConfig; use serde::Serialize; /// Dónde busca el árbol de Kconfig si no se pasa `--kconfig`. const KCONFIG_ENV: &str = "HAMMER_KCONFIG_ROOT"; #[derive(Subcommand)] pub enum KernelCmd { /// Analiza el grafo de Kconfig: qué muere si se apagan estos símbolos, y por dónde se cuela un /// `select` de vuelta. /// /// Es la herramienta con la que se decide si un bundle «por clausura» es viable (SDD 22 §3). /// No construye ni escribe nada. Closure { /// Símbolos raíz a apagar, SIN el prefijo `CONFIG_` (p.ej. `WIRELESS`). #[arg(required = true)] symbols: Vec, /// Raíz del árbol de fuentes del kernel (el directorio con el `Kconfig` de arriba). #[arg(long)] kconfig: Option, /// Contrasta la clausura contra los símbolos apagados en este `.config`. #[arg(long)] against: Option, /// Cierra la clausura contra sus propias fugas: apaga también a cada selector, y repite. /// Muestra el PRECIO (qué hubo que tragarse de más) en vez de aplicarlo a ciegas. #[arg(long)] fixpoint: bool, /// Emite JSON en vez del informe legible. #[arg(long)] json: bool, /// Cuántos símbolos de la clausura listar en el informe legible. #[arg(long, default_value_t = 20)] limit: usize, }, /// Estadísticas del árbol de Kconfig parseado (control de salud del lector). Stats { #[arg(long)] kconfig: Option, /// Muestra las primeras N advertencias del parseo. #[arg(long, default_value_t = 10)] warnings: usize, }, } pub fn run(cmd: KernelCmd) -> Result<()> { match cmd { KernelCmd::Stats { kconfig, warnings } => stats(&kconfig_root(kconfig)?, warnings), KernelCmd::Closure { symbols, kconfig, against, fixpoint, json, limit, } => closure( &symbols, &kconfig_root(kconfig)?, against.as_deref(), fixpoint, json, limit, ), } } /// Resuelve la raíz del árbol de Kconfig: `--kconfig` gana, si no la variable de entorno. fn kconfig_root(flag: Option) -> Result { if let Some(p) = flag { return Ok(p); } match std::env::var_os(KCONFIG_ENV) { Some(v) => Ok(PathBuf::from(v)), None => anyhow::bail!( "falta el árbol de fuentes del kernel: pasá --kconfig o exportá {KCONFIG_ENV}" ), } } fn load_tree(root: &Path) -> Result { if !root.join("Kconfig").is_file() { anyhow::bail!( "{} no parece un árbol de fuentes de kernel (falta ./Kconfig).\n\ Pasá --kconfig o exportá {KCONFIG_ENV}.", root.display() ); } KconfigTree::parse(root, &KconfigTree::vars_x86_64()) .with_context(|| format!("parseando {}", root.display())) } fn stats(root: &Path, show_warnings: usize) -> Result<()> { let t = load_tree(root)?; let visible = t.symbols.values().filter(|s| s.is_visible()).count(); let multi = t.symbols.values().filter(|s| s.defs.len() > 1).count(); let with_help = t.symbols.values().filter(|s| s.help().is_some()).count(); let selects: usize = t.symbols.values().map(|s| s.select_targets().len()).sum(); let implies: usize = t.symbols.values().map(|s| s.imply_targets().len()).sum(); println!("árbol {}", root.display()); println!("ficheros {}", t.files.len()); println!("símbolos {}", t.symbols.len()); println!(" visibles {visible} (marcables en menuconfig)"); println!(" con ayuda {with_help}"); println!(" definidos en >1 sitio {multi}"); println!("aristas select {selects} · imply {implies}"); println!("avisos {}", t.warnings.len()); for w in t.warnings.iter().take(show_warnings) { println!(" · {w}"); } if t.warnings.len() > show_warnings { println!(" … y {} más", t.warnings.len() - show_warnings); } Ok(()) } #[derive(Serialize)] struct ClosureReport { roots: Vec, /// Todo lo que queda inalcanzable si se apagan las raíces (incluidas las raíces). closure: Vec, /// De la clausura, los que un humano puede marcar en `menuconfig`. visible: usize, /// `select` que entran desde fuera: reencienden algo de la clausura ignorando sus `depends on`. leaks: Vec, /// Símbolos distintos de la clausura alcanzados por esas fugas. leaked_targets: usize, #[serde(skip_serializing_if = "Option::is_none")] fixpoint: Option, #[serde(skip_serializing_if = "Option::is_none")] against: Option, } /// El precio de cerrar el bundle contra sus fugas. #[derive(Serialize)] struct FixpointReport { /// La clausura ya cerrada, entera. closure: Vec, /// Tamaño de la clausura una vez cerrada. closure_len: usize, /// Cuánto creció respecto de la clausura estricta. grew_by: usize, rounds: usize, /// Lo que hubo que apagar de más, con la ronda en que apareció. added: Vec<(usize, String)>, /// Fugas que sobrevivieron al tope de rondas. Vacío = cerró. remaining_leaks: Vec, } /// Contraste contra un `.config` real: la validación §3 del SDD 22. #[derive(Serialize)] struct AgainstReport { config: String, /// De la clausura, los que el `.config` tiene ENCENDIDOS (la clausura predice de más, o el /// config tiene una fuga real). on_in_config: Vec, /// De la clausura, los que el `.config` apaga explícitamente. off_in_config: usize, /// De la clausura, los que el `.config` ni menciona. absent_in_config: usize, } fn closure( roots: &[String], kroot: &Path, against: Option<&Path>, fixpoint: bool, json: bool, limit: usize, ) -> Result<()> { let t = load_tree(kroot)?; // Avisar de raíces que no existen: un símbolo mal escrito daría clausura vacía en silencio, // que es exactamente el modo de fallo que este comando existe para no tener. let desconocidas: Vec<&String> = roots.iter().filter(|r| !t.symbols.contains_key(*r)).collect(); if !desconocidas.is_empty() { eprintln!( "aviso: {} símbolo(s) no existen en este árbol: {}", desconocidas.len(), desconocidas .iter() .map(|s| s.as_str()) .collect::>() .join(", ") ); } let cl = t.closure_off(roots); let leaks = t.select_leaks(&cl); let leaked_targets = leaks .iter() .map(|l| l.target.as_str()) .collect::>() .len(); let visible = cl .iter() .filter(|s| t.symbols.get(*s).is_some_and(|s| s.is_visible())) .count(); let fx = fixpoint.then(|| { let f = t.closure_off_fixpoint(roots, 16); FixpointReport { closure_len: f.closure.len(), closure: f.closure.iter().cloned().collect(), grew_by: f.closure.len().saturating_sub(cl.len()), rounds: f.rounds, added: f.added, remaining_leaks: f.remaining_leaks, } }); let against_report = match against { None => None, Some(p) => { let text = read_maybe_gz(p)?; let cfg = KernelConfig::parse(&text); let on: Vec = cl.iter().filter(|s| cfg.is_on(s)).cloned().collect(); let off = cl .iter() .filter(|s| matches!(cfg.get(s), Some(hammer_core::kernel::ConfigValue::No))) .count(); let absent = cl.iter().filter(|s| cfg.get(s).is_none()).count(); Some(AgainstReport { config: p.display().to_string(), on_in_config: on, off_in_config: off, absent_in_config: absent, }) } }; let rep = ClosureReport { roots: roots.to_vec(), closure: cl.iter().cloned().collect(), visible, leaks, leaked_targets, fixpoint: fx, against: against_report, }; if json { println!("{}", serde_json::to_string_pretty(&rep)?); return Ok(()); } println!("raíces apagadas {}", rep.roots.join(" ")); println!( "clausura {} símbolo(s) ({} visibles en menuconfig)", rep.closure.len(), rep.visible ); for s in rep.closure.iter().take(limit) { let prompt = t .symbols .get(s) .and_then(|x| x.prompt()) .unwrap_or("(sin prompt: sólo entra por select)"); println!(" · {s} — {prompt}"); } if rep.closure.len() > limit { println!(" … y {} más", rep.closure.len() - limit); } println!(); if rep.leaks.is_empty() { println!("fugas por `select` ninguna — la clausura es cerrada"); } else { println!( "fugas por `select` {} arista(s) desde fuera reencienden {} símbolo(s) de la clausura", rep.leaks.len(), rep.leaked_targets ); println!(" (`select` FUERZA el destino ignorando sus `depends on`: apagar la raíz no basta)"); let mut por_destino: BTreeMap<&str, Vec<&str>> = BTreeMap::new(); for l in &rep.leaks { por_destino .entry(&l.target) .or_default() .push(&l.selector); } for (target, selectors) in por_destino.iter().take(limit) { println!(" · {target} ← {}", selectors.join(", ")); } if por_destino.len() > limit { println!(" … y {} destino(s) más", por_destino.len() - limit); } } if let Some(f) = &rep.fixpoint { println!(); println!( "punto fijo {} símbolo(s) tras {} ronda(s) — {:+} sobre la clausura estricta", f.closure_len, f.rounds, f.grew_by as i64 ); if f.remaining_leaks.is_empty() { println!(" cerró: ninguna fuga sobrevive"); } else { println!( " ⚠ NO cerró: {} fuga(s) siguen abiertas tras el tope de rondas", f.remaining_leaks.len() ); } if f.added.is_empty() { println!(" no hizo falta apagar nada de más"); } else { println!(" precio: {} símbolo(s) apagados de más", f.added.len()); for (r, s) in f.added.iter().take(limit) { println!(" · ronda {r}: {s}"); } if f.added.len() > limit { println!(" … y {} más", f.added.len() - limit); } } } if let Some(a) = &rep.against { println!(); println!("contraste contra {}", a.config); println!(" apagados explícitos {}", a.off_in_config); println!(" ausentes {}", a.absent_in_config); println!(" ENCENDIDOS {}", a.on_in_config.len()); for s in a.on_in_config.iter().take(limit) { println!(" · {s}"); } if a.on_in_config.len() > limit { println!(" … y {} más", a.on_in_config.len() - limit); } } Ok(()) } /// Lee un `.config`, descomprimiendo si es `.gz` (el caso de `/proc/config.gz`). /// /// Se delega a `gzip -dc` en vez de arrastrar un descompresor al core: es una entrada de /// diagnóstico, no del camino de build, y el core queda sin dependencias nuevas. pub fn read_maybe_gz(p: &Path) -> Result { if p.extension().is_some_and(|e| e == "gz") { let out = std::process::Command::new("gzip") .arg("-dc") .arg(p) .output() .with_context(|| format!("descomprimiendo {}", p.display()))?; if !out.status.success() { anyhow::bail!( "gzip -dc {} falló: {}", p.display(), String::from_utf8_lossy(&out.stderr).trim() ); } return Ok(String::from_utf8_lossy(&out.stdout).into_owned()); } std::fs::read_to_string(p).with_context(|| format!("leyendo {}", p.display())) }