kernel: el lector de Kconfig y la validación del §3 — la clausura reproduce los bundles a mano
Primer paso del SDD 22 (armador de kernel), en el orden que fija su §8. Regla dura
respetada literalmente: hammer LEE el grafo de Kconfig, no lo resuelve — el .config lo
sigue produciendo el olddefconfig del propio kernel.
hammer-core/src/kernel/: lector tolerante (18.212 símbolos, 1646 ficheros, 0 avisos de
parseo sobre 6.16.12) + lector de .config. hammer kernel {stats,closure}.
La semántica de arista, que el §3 pedía definir antes de escribir el predicado:
· dependencia dura = símbolo en posición CONJUNTIVA (en "A && (B|C)" sólo A). La
disyunción, la negación y las comparaciones no aportan. Conservador a propósito:
apagar de menos se nota, apagar de más hace un ladrillo.
· símbolo con varias definiciones ⇒ INTERSECCIÓN entre ellas, no unión.
· select es el portillo, no una arista más: fuerza el destino IGNORANDO sus depends.
select_leaks las enumera; closure_off_fixpoint cierra el bundle contra ellas y REPORTA
el precio en vez de aplicarlo solo.
La medición que decide §2.1, contra el bundle N1 hecho a mano de recipes/linux.toml:
clausura estricta de WIRELESS ......................... 350
punto fijo (3 fugas: WLAN, IWLEGACY, GELIC_WIRELESS) .. 406, cierra en 1 ronda
bundle a mano ......................................... 421
SOBRA 0 · falta 15
Los 15 son todos RFKILL, que no es wifi sino el interruptor de radio compartido con
bluetooth y NFC. El humano apagó DOS bundles en la misma línea ⇒ el catálogo necesita
"sin radios" como entrada propia. §2.1 es viable.
Y el punto fijo también dice cuándo no: cerrar "sin audio" exige tragarse DRM_I915/
NOUVEAU/AMD_DC, que hacen select del códec HDMI. En linux.toml sale gratis porque los
gráficos ya están apagados; en un escritorio sería una decisión.
De regalo: linux.toml apaga REISERFS_FS, que 6.16.12 ya no tiene. Un -d a un símbolo
inexistente se pierde HOY en silencio — justo lo que el diff-back (paso 4) va a atrapar.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
//! `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<String>,
|
||||
/// Raíz del árbol de fuentes del kernel (el directorio con el `Kconfig` de arriba).
|
||||
#[arg(long)]
|
||||
kconfig: Option<PathBuf>,
|
||||
/// Contrasta la clausura contra los símbolos apagados en este `.config`.
|
||||
#[arg(long)]
|
||||
against: Option<PathBuf>,
|
||||
/// 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<PathBuf>,
|
||||
/// 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<PathBuf>) -> Result<PathBuf> {
|
||||
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 <dir> o exportá {KCONFIG_ENV}"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_tree(root: &Path) -> Result<KconfigTree> {
|
||||
if !root.join("Kconfig").is_file() {
|
||||
anyhow::bail!(
|
||||
"{} no parece un árbol de fuentes de kernel (falta ./Kconfig).\n\
|
||||
Pasá --kconfig <dir> 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<String>,
|
||||
/// Todo lo que queda inalcanzable si se apagan las raíces (incluidas las raíces).
|
||||
closure: Vec<String>,
|
||||
/// 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<hammer_core::kernel::SelectLeak>,
|
||||
/// Símbolos distintos de la clausura alcanzados por esas fugas.
|
||||
leaked_targets: usize,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
fixpoint: Option<FixpointReport>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
against: Option<AgainstReport>,
|
||||
}
|
||||
|
||||
/// El precio de cerrar el bundle contra sus fugas.
|
||||
#[derive(Serialize)]
|
||||
struct FixpointReport {
|
||||
/// La clausura ya cerrada, entera.
|
||||
closure: Vec<String>,
|
||||
/// 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<hammer_core::kernel::SelectLeak>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
/// 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::<Vec<_>>()
|
||||
.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::<BTreeSet<_>>()
|
||||
.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<String> = 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<String> {
|
||||
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()))
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use std::path::PathBuf;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
mod alpine_import;
|
||||
mod kernel_cmd;
|
||||
mod nix_import;
|
||||
|
||||
const DEFAULT_STORE: &str = "/store";
|
||||
@@ -446,6 +447,13 @@ enum Cmd {
|
||||
#[command(subcommand)]
|
||||
sub: BootCmd,
|
||||
},
|
||||
/// [SDD 22] El armador de kernel: analiza el grafo de Kconfig, lee el kernel que ya corre y
|
||||
/// emite recetas derivadas. **Nunca escribe un `.config`**: eso lo hace el `olddefconfig` del
|
||||
/// propio kernel.
|
||||
Kernel {
|
||||
#[command(subcommand)]
|
||||
sub: kernel_cmd::KernelCmd,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -1414,6 +1422,7 @@ fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Cmd::Kernel { sub } => kernel_cmd::run(sub)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
//! Lectura de un `.config` de kernel (o del `/proc/config.gz` ya descomprimido).
|
||||
//!
|
||||
//! Es el sustrato del **modo reversa** (#9 del handoff): antes de compilar nada, leer el kernel que
|
||||
//! ya corre y mirarlo a través del lente de bundles. Cero riesgo, cero build.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Valor de un símbolo en un `.config` materializado.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ConfigValue {
|
||||
/// `=y`
|
||||
Yes,
|
||||
/// `=m`
|
||||
Module,
|
||||
/// `# CONFIG_X is not set` — presencia explícita del apagado, que **no** es lo mismo que
|
||||
/// ausencia: un símbolo ausente puede no existir en esta versión del kernel.
|
||||
No,
|
||||
/// `="texto"`
|
||||
Str(String),
|
||||
/// `=42`, `=0x10`
|
||||
Num(String),
|
||||
}
|
||||
|
||||
impl ConfigValue {
|
||||
/// ¿Está presente en el kernel, como built-in o como módulo?
|
||||
pub fn is_on(&self) -> bool {
|
||||
matches!(self, ConfigValue::Yes | ConfigValue::Module)
|
||||
}
|
||||
}
|
||||
|
||||
/// Un `.config` leído. Las claves van **sin** el prefijo `CONFIG_`, igual que en los ficheros
|
||||
/// Kconfig, para que un símbolo se llame igual en los dos lados del análisis.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct KernelConfig {
|
||||
pub values: BTreeMap<String, ConfigValue>,
|
||||
/// La línea `# Linux/x86 6.16.12 Kernel Configuration`, si está.
|
||||
pub banner: Option<String>,
|
||||
}
|
||||
|
||||
impl KernelConfig {
|
||||
pub fn parse(text: &str) -> KernelConfig {
|
||||
let mut cfg = KernelConfig::default();
|
||||
for line in text.lines() {
|
||||
let t = line.trim();
|
||||
if t.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = t.strip_prefix("# CONFIG_") {
|
||||
if let Some(name) = rest.strip_suffix(" is not set") {
|
||||
cfg.values.insert(name.to_string(), ConfigValue::No);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = t.strip_prefix('#') {
|
||||
let rest = rest.trim();
|
||||
if cfg.banner.is_none() && rest.contains("Kernel Configuration") {
|
||||
cfg.banner = Some(rest.to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let Some(rest) = t.strip_prefix("CONFIG_") else {
|
||||
continue;
|
||||
};
|
||||
let Some((name, val)) = rest.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
let v = match val {
|
||||
"y" => ConfigValue::Yes,
|
||||
"m" => ConfigValue::Module,
|
||||
"n" => ConfigValue::No,
|
||||
_ if val.starts_with('"') => {
|
||||
ConfigValue::Str(val.trim_matches('"').to_string())
|
||||
}
|
||||
_ => ConfigValue::Num(val.to_string()),
|
||||
};
|
||||
cfg.values.insert(name.to_string(), v);
|
||||
}
|
||||
cfg
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Option<&ConfigValue> {
|
||||
self.values.get(name)
|
||||
}
|
||||
|
||||
/// `true` sólo si el símbolo está `=y` o `=m`. Ausente cuenta como apagado.
|
||||
pub fn is_on(&self, name: &str) -> bool {
|
||||
self.values.get(name).is_some_and(|v| v.is_on())
|
||||
}
|
||||
|
||||
/// Los símbolos encendidos (`=y` o `=m`), en orden.
|
||||
pub fn enabled(&self) -> impl Iterator<Item = &str> {
|
||||
self.values
|
||||
.iter()
|
||||
.filter(|(_, v)| v.is_on())
|
||||
.map(|(k, _)| k.as_str())
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.values.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.values.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SAMPLE: &str = r#"
|
||||
#
|
||||
# Automatically generated file; DO NOT EDIT.
|
||||
# Linux/x86 6.16.12 Kernel Configuration
|
||||
#
|
||||
CONFIG_CC_VERSION_TEXT="gcc (GCC) 15.1.1"
|
||||
CONFIG_64BIT=y
|
||||
CONFIG_WLAN=m
|
||||
# CONFIG_WIRELESS is not set
|
||||
CONFIG_NR_CPUS=64
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn parsea_las_cuatro_formas() {
|
||||
let c = KernelConfig::parse(SAMPLE);
|
||||
assert_eq!(c.get("64BIT"), Some(&ConfigValue::Yes));
|
||||
assert_eq!(c.get("WLAN"), Some(&ConfigValue::Module));
|
||||
assert_eq!(c.get("WIRELESS"), Some(&ConfigValue::No));
|
||||
assert_eq!(c.get("NR_CPUS"), Some(&ConfigValue::Num("64".into())));
|
||||
assert!(matches!(c.get("CC_VERSION_TEXT"), Some(ConfigValue::Str(_))));
|
||||
assert_eq!(c.banner.as_deref(), Some("Linux/x86 6.16.12 Kernel Configuration"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modulo_cuenta_como_encendido() {
|
||||
let c = KernelConfig::parse(SAMPLE);
|
||||
assert!(c.is_on("WLAN"));
|
||||
assert!(!c.is_on("WIRELESS"));
|
||||
// Ausente ≠ apagado explícito, pero para `is_on` los dos son «no está».
|
||||
assert!(!c.is_on("NO_EXISTE"));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
//! El armador de kernel: catálogo de bundles, clausuras sobre Kconfig y modo reversa.
|
||||
//!
|
||||
//! Implementa `docs/22-configurador-kernel.md`, que a su vez contesta
|
||||
//! `tawasuyu/HANDOFF-KERNEL-CONFIG-A-HAMMER.md`.
|
||||
//!
|
||||
//! **El hecho que ordena todo el módulo** (SDD 22 §1): las fases de build entran en
|
||||
//! `Recipe::hash_inputs`, y el config del kernel vive en la fase `configure` ⇒ **el config ES la
|
||||
//! identidad del artefacto**. Cambiar un símbolo cambia el `ArtifactHash`. De ahí dos consecuencias
|
||||
//! que el código respeta literalmente:
|
||||
//!
|
||||
//! 1. Una «perilla» de la UI **no es un parámetro de runtime, es una edición de receta**. Por eso
|
||||
//! `plan` emite una **receta derivada**, no un binario parametrizable.
|
||||
//! 2. Atestar «este config booteó en esta huella» sale casi gratis: ya es un artefacto CAS.
|
||||
//!
|
||||
//! Y la regla dura que nunca se cruza: **hammer no resuelve Kconfig**. Emite fragmentos
|
||||
//! (`scripts/config -e/-d`) y deja que el `olddefconfig` del propio kernel produzca el `.config`.
|
||||
//! Ver [`kconfig`].
|
||||
|
||||
pub mod config;
|
||||
pub mod kconfig;
|
||||
|
||||
pub use config::{ConfigValue, KernelConfig};
|
||||
pub use kconfig::{Expr, KconfigTree, SelectLeak, SymKind, Symbol};
|
||||
@@ -10,6 +10,7 @@ pub mod compat;
|
||||
pub mod differs;
|
||||
pub mod hash;
|
||||
pub mod installed;
|
||||
pub mod kernel;
|
||||
pub mod proto;
|
||||
pub mod query;
|
||||
pub mod recipe;
|
||||
|
||||
@@ -221,3 +221,91 @@ De acuerdo con el reparto tal como está. Dos precisiones desde este lado:
|
||||
|
||||
**No implementar todavía.** Este documento existe para que la decisión se tome con los seis datos
|
||||
verificados, no con las suposiciones del handoff — que su autor, correctamente, marcó como tales.
|
||||
|
||||
---
|
||||
|
||||
# ADENDA — 2026-08-10: implementación en marcha
|
||||
|
||||
El usuario decidió. Lo que sigue ya no es análisis: es lo medido contra el código escrito. Se sigue
|
||||
el orden del §8.
|
||||
|
||||
## 9. Paso 2 hecho: la clausura SÍ reproduce los bundles a mano
|
||||
|
||||
`hammer kernel` (`crates/hammer-core/src/kernel/`, `crates/hammer-cli/src/kernel_cmd.rs`) trae un
|
||||
lector de Kconfig **de sólo análisis** — la regla dura del §6 del handoff se respeta literalmente: no
|
||||
hay solver, el `.config` lo sigue produciendo `olddefconfig`.
|
||||
|
||||
Salud del lector, sobre `linux-6.16.12` (`hammer kernel stats`):
|
||||
|
||||
| ficheros | símbolos | visibles | con ayuda | aristas `select` | aristas `imply` | avisos de parseo |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1646 | 18 212 | 15 169 | 14 866 | 15 165 | 445 | **0** |
|
||||
|
||||
### La semántica de arista, resuelta
|
||||
El §3 pedía definirla antes de escribir el predicado. Quedó así, y las dos mitades importan:
|
||||
|
||||
- **`depends on` en posición conjuntiva** — sólo cuenta como dependencia dura el símbolo que, puesto
|
||||
a `n`, apaga la expresión entera. En `A && (B || C)` sólo `A` es duro; `!X`, las ramas de un `||` y
|
||||
los dos lados de una comparación no aportan nada. Es deliberadamente conservador: preferimos un
|
||||
bundle que apague **de menos** (se nota) a uno que apague de más (hace un ladrillo).
|
||||
- **Un símbolo con varias definiciones** muere sólo si mueren todas ⇒ las dependencias duras son la
|
||||
**intersección entre definiciones**, no la unión. El error natural (la unión) diría que apagar `Y`
|
||||
mata a `X` cuando `X` tiene otra definición que ni menciona a `Y`.
|
||||
- **`select` no es una arista más: es el portillo.** Fuerza el destino **ignorando** sus
|
||||
`depends on`, así que un símbolo de fuera del bundle reenciende lo que el bundle apagó.
|
||||
`select_leaks` las enumera, y `closure_off_fixpoint` cierra el bundle contra ellas — **reportando
|
||||
el precio, nunca aplicándolo solo**.
|
||||
|
||||
### La medición que decide §2.1
|
||||
Corpus: el juego de bundles N1 escrito a mano en la fase `configure` de `recipes/linux.toml`, sobre
|
||||
un kernel que arranca. Pregunta: *¿la clausura calculada desde la raíz mínima reproduce lo que
|
||||
escribió el humano?*
|
||||
|
||||
```
|
||||
hammer kernel closure --kconfig <árbol> WIRELESS --fixpoint
|
||||
```
|
||||
|
||||
| | símbolos |
|
||||
|---|---|
|
||||
| clausura estricta de `WIRELESS` (sólo `depends on`) | 350 |
|
||||
| punto fijo (cierra las 3 fugas `select`: `WLAN`, `IWLEGACY`, `GELIC_WIRELESS`) | **406** |
|
||||
| bundle `-d WLAN -d WIRELESS -d CFG80211 -d MAC80211 -d RFKILL` hecho a mano | **421** |
|
||||
| **sobra** (apagaría algo que el humano dejó vivo) | **0** |
|
||||
| **falta** | **15** |
|
||||
|
||||
**Sobra cero.** La clausura no toca un solo símbolo que el humano quisiera conservar — que era el
|
||||
riesgo real, porque sobrar es hacer un ladrillo. Cierra en **una ronda**.
|
||||
|
||||
Los 15 que faltan son **todos de `RFKILL`** (`RFKILL{,_GPIO,_INPUT,_LEDS}` y los drivers de laptop
|
||||
que dependen de él: `DELL_RBTN`, `IDEAPAD_LAPTOP`, `MSI_LAPTOP`, `AMILO_RFKILL`…). Y `RFKILL` **no
|
||||
es wifi**: es el interruptor de radio compartido, que bluetooth y NFC declaran con el mismo idioma
|
||||
(`depends on RFKILL || !RFKILL` en `net/{bluetooth,nfc,wireless}/Kconfig`). Que quede fuera no es un
|
||||
fallo de la clausura — es que el humano apagó **dos** bundles a la vez («no necesito wifi» y «no
|
||||
necesito radios») y los escribió en la misma línea. ⇒ **el catálogo N1 necesita «sin radios» como
|
||||
entrada propia.** Nótese que ese idioma es un `||`: la regla conservadora lo excluye de los duros,
|
||||
que es exactamente lo correcto.
|
||||
|
||||
⇒ **§2.1 es viable y la objeción del §3 queda resuelta**, con una corrección al diseño: un bundle no
|
||||
es `clausura(raíz)` sino **`clausura(raíz)` + el punto fijo sobre `select`**, y las fugas se
|
||||
aprueban a mano una vez, no en cada release.
|
||||
|
||||
### Y el punto fijo también dice cuándo NO
|
||||
No siempre cierra barato, y eso es información, no un fallo. Medido sobre los otros tres bundles a
|
||||
mano de `linux.toml`:
|
||||
|
||||
| bundle a mano | clausura | fugas `select` | qué las causa |
|
||||
|---|---|---|---|
|
||||
| `WLAN WIRELESS CFG80211 MAC80211 RFKILL` | 421 | **0** | cerrado |
|
||||
| `SOUND SND` | 1323 | 19 sobre 7 destinos | `DRM_I915`/`DRM_NOUVEAU`/`DRM_AMD_DC` hacen `select` del códec HDMI |
|
||||
| `DRM DRM_I915 … FB AGP` | 770 | 15 sobre 6 destinos | drivers USB-C (`TYPEC_*`) hacen `select DRM_AUX_BRIDGE` |
|
||||
| `XFS_FS BTRFS_FS … NTFS3_FS` | 46 | 1 | `NTFS3_FS ← NTFS_FS` |
|
||||
|
||||
Para cerrar «no necesito audio» hay que tragarse drivers gráficos. En `linux.toml` sale gratis
|
||||
porque los gráficos también están apagados, pero en un perfil de escritorio **no**: ahí la fuga es
|
||||
una decisión, y por eso el punto fijo la muestra en vez de aplicarla.
|
||||
|
||||
### Un hallazgo de regalo
|
||||
`recipes/linux.toml` apaga **`REISERFS_FS`, que no existe en 6.16.12** (upstream lo retiró). El
|
||||
`scripts/config -d REISERFS_FS` es un no-op que nadie vio. Es exactamente el modo de fallo que el
|
||||
**diff-back** (paso 4 del §8) existe para atrapar, encontrado antes de implementarlo: hoy un
|
||||
`-e`/`-d` sobre un símbolo inexistente **se pierde en silencio**.
|
||||
|
||||
Reference in New Issue
Block a user