kernel: la clausura contrastada contra un olddefconfig de verdad — 65/68 con 0 falsos positivos

Hasta acá el armador era análisis puro: la clausura decía qué debía morir y nadie lo había
contrastado con el resolvedor real. NO hace falta construir un kernel para hacerlo: lo caro
es la fase compile (35-60 min); toda la cadena del armador vive en configure y corre en
segundos.

Método: árbol 6.16.12 entero, `make defconfig` de base (el config de linux.toml NO sirve de
base: ya apaga wifi/audio/fs a mano, así que el lado disable no apagaría nada y la
predicción no se pondría a prueba — fue mi primer error), el fragmento del plan, y
olddefconfig. Verdad de campo = los símbolos que pasaron de encendidos a apagados.

  sólo depends on ....... clausura 1775  aciertos 64/68  SOBRA 0  falta 4
  + huérfanos select .... clausura 1794  aciertos 65/68  SOBRA 0  falta 3
  + comparaciones ....... clausura 1795  aciertos 65/68  SOBRA 0  falta 3

SOBRA 0 en las tres: el predictor nunca dice que muere algo que sobrevive, que es la única
dirección en la que puede equivocarse sin fabricar un ladrillo.

Dos refinamientos que salieron de la medición, cada uno con su test:
  · HUÉRFANOS DE SELECT. Un símbolo sin prompt no se marca a mano: sólo entra por select.
    Si caen todos sus selectores, cae él, aunque nadie dependa de él (caso ACPI_NHLT). Se
    exige >=1 selector: sin ninguno entra por un default, y darlo por muerto mataría media
    tabla.
  · `X = y` SÍ ES DEPENDENCIA DURA. Medio drivers/video/fbdev declara su dependencia de FB
    como `depends on (FB = y) && ARM`. Tratar toda comparación como opaca dejaba esos
    drivers fuera. `X = n` sigue fuera a propósito: con X en n es VERDADERA. De regalo, las
    fugas select sin declarar de sin-graficos cayeron de 8+ a 1.

Los 3 que faltan NO son un fallo, son otra pregunta: CRYPTO_LIB_ARC4, REGMAP y
SYSTEM_DATA_VERIFICATION tienen selectores FUERA de la clausura (PPP_MPPE, 111 usuarios más
de REGMAP…). Se quedaron sin usuarios en ESE config; encendés PPP y ARC4 vuelve.
«Inalcanzable» y «apagado ahora» no son lo mismo, y la clausura contesta la primera.

Y un bug que sólo aparece corriendo el resolvedor: el diff-back contaba como promesa
incumplida todo símbolo pedido ausente del .config. Pero Kconfig NO EMITE un símbolo cuyas
dependencias no se cumplen ⇒ un `-d WLAN` cuya raíz ya cayó simplemente no sale. Con esa
cuenta un plan perfecto se reportaba roto (2 falsos incumplidos de 16). Ahora: ausente +
se pedía apagar = éxito; ausente + se pedía encender = fallo. La corrida real sale 14
cumplidos, 0 incumplidos.

GUARDIÁN NUEVO, y hacía falta: las cuatro recetas de kernel NO son el mismo kernel — linux
y linux-metal van por 6.16.12, linux-metal-dual y linux-generic por 7.1.2. Planear una
contra el árbol de la otra calcularía clausuras sobre símbolos que ahí no existen, y
saldría SIN RUIDO. KconfigTree lee ahora su versión del Makefile de arriba y `plan` FALLA
si no coincide con la de la receta (los diagnósticos sólo avisan). Aviso de la sesión de
granja/store, verificado antes de implementarlo.

Catálogo: las dos fugas que destapó la clausura más grande quedan declaradas con motivo
(FB_SYSMEM_HELPERS_DEFERRED por HID_PICOLCD_FB; DRM_DISPLAY_DP_TUNNEL_STATE_DEBUG por
DRM_I915_DEBUG), y las notas sobre THUNDERBOLT/REISERFS_FS pasan a pasado: ya se
corrigieron en 2602218.

Runbook §4.bis: cómo probar un plan entero en 30 s en vez de 40 min.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-08-10 14:20:52 +00:00
co-authored by Claude Opus 5
parent b88930234f
commit ca9bde0251
6 changed files with 321 additions and 29 deletions
+32 -3
View File
@@ -907,6 +907,29 @@ fn plan(
let tree = load_tree(kroot)?;
let cat = hammer_core::kernel::Catalog::load(catalog)?;
// Las recetas de kernel del repo NO son todas la misma versión (`linux`/`linux-metal` van por
// 6.16.12; `linux-metal-dual`/`linux-generic` por 7.1.2). Planear una contra el árbol de la otra
// calcularía clausuras sobre símbolos que ahí no existen, y saldría SIN RUIDO. Como el plan
// determina un artefacto, acá se falla en vez de avisar.
match tree.version.as_deref() {
Some(v) if v != base.version => anyhow::bail!(
"la receta «{}» es del kernel {} y el árbol de {} es el {v}.\n\
Las clausuras saldrían calculadas sobre otro juego de símbolos. Pasá el árbol correcto \
con --kconfig.",
base.name,
base.version,
kroot.display()
),
None => eprintln!(
"aviso: {} no trae el Makefile de arriba, así que no puedo comprobar que sea el kernel \
{} de la receta «{}»",
kroot.display(),
base.version,
base.name
),
Some(_) => {}
}
let mut p = hammer_core::kernel::plan::make_plan(
&base,
&recipe_path.display().to_string(),
@@ -1025,9 +1048,15 @@ fn diff_back(plan_path: &Path, config: &Path, json: bool) -> Result<()> {
println!("plan {} ({})", p.derived_name, plan_path.display());
println!("config {}", config.display());
println!("cumplidos {}", d.honored.len());
println!("ausentes {}", d.absent.len());
for s in &d.absent {
println!(" {s} — el kernel ni lo menciona: la bandera fue un no-op");
// Kconfig no emite un símbolo cuyas dependencias no se cumplen: para un `-d`, que no
// aparezca ES el resultado buscado. Contarlo como fallo reportaba planes perfectos como
// rotos — lo aprendí corriendo un olddefconfig de verdad.
println!("apagados por dependencia {} (no aparecen porque cayeron con su raíz)", d.off_by_dependency.len());
if !d.never_existed.is_empty() {
println!("no existen en este árbol {}", d.never_existed.len());
for s in &d.never_existed {
println!(" {s} — la bandera no hace nada");
}
}
println!("INCUMPLIDOS {}", d.violated.len());
for v in &d.violated {
+159 -8
View File
@@ -124,7 +124,29 @@ impl Expr {
a.hard_symbols(out);
b.hard_symbols(out);
}
Expr::Const(_) | Expr::Not(_) | Expr::Or(_, _) | Expr::Cmp(_, _, _) => {}
// `X = y`, `X = m` y `X != n` SÍ son duras: si `X` vale `n` la comparación es falsa y
// se lleva la conjunción entera. Es la forma con la que medio `drivers/video/fbdev`
// declara su dependencia de `FB` (`depends on (FB = y) && ARM`), así que tratarlas como
// opacas dejaba fuera de la clausura a drivers que sí mueren.
// `X = n` queda fuera a propósito: con `X` en `n` es VERDADERA.
Expr::Cmp(a, op, b) => {
let par = match (a.as_ref(), b.as_ref()) {
(Expr::Sym(s), Expr::Const(c)) => Some((s, c.as_str())),
(Expr::Const(c), Expr::Sym(s)) => Some((s, c.as_str())),
_ => None,
};
if let Some((s, c)) = par {
let dura = match op {
CmpOp::Eq => c == "y" || c == "m",
CmpOp::Ne => c == "n",
_ => false,
};
if dura {
out.insert(s.clone());
}
}
}
Expr::Const(_) | Expr::Not(_) | Expr::Or(_, _) => {}
}
}
}
@@ -239,6 +261,14 @@ pub struct KconfigTree {
pub symbols: BTreeMap<String, Symbol>,
/// Ficheros efectivamente leídos (relativos a la raíz).
pub files: Vec<String>,
/// Versión que el árbol declara de sí mismo (`VERSION.PATCHLEVEL.SUBLEVEL` del `Makefile` de
/// arriba). `None` si no se extrajo el `Makefile`.
///
/// No es cosmética: **las recetas de kernel del repo no son todas la misma versión** (`linux` y
/// `linux-metal` van por 6.16.12; `linux-metal-dual` y `linux-generic` por 7.1.2). Planear una
/// contra el árbol de la otra daría clausuras calculadas sobre símbolos que ahí no existen —
/// y saldría sin ruido, que es el modo de fallo que este campo existe para impedir.
pub version: Option<String>,
/// Todo lo que no se pudo interpretar, contado en vez de abortado.
pub warnings: Vec<String>,
/// `Y → {X : X depende DURO de Y}`. Apagar `Y` deja a cada `X` inalcanzable.
@@ -265,6 +295,7 @@ impl KconfigTree {
visited: &mut visited,
};
parser.parse_file("Kconfig", &[])?;
tree.version = read_makefile_version(root);
tree.build_indices();
Ok(tree)
}
@@ -309,9 +340,26 @@ impl KconfigTree {
.unwrap_or_else(|| EMPTY.get_or_init(BTreeSet::new))
}
/// **La clausura**: todo lo que se vuelve inalcanzable si se apagan `roots`.
/// **La clausura**: todo lo que se vuelve **inalcanzable** si se apagan `roots`.
///
/// Es el cierre transitivo de `depends on` en dirección inversa. Incluye a las propias raíces.
/// Ojo con la palabra: *inalcanzable*, no *apagado ahora*. Son dos preguntas distintas y
/// confundirlas es el error natural (ver la nota de abajo). Ésta contesta «¿puede este símbolo
/// volver a encenderse alguna vez?», que es la que define un bundle.
///
/// Dos mecanismos, ambos necesarios (medidos contra un `olddefconfig` real, SDD 22 §12):
/// 1. **Cierre transitivo de `depends on` en inversa** — si muere de quien dependo, muero.
/// 2. **Huérfanos de `select`** — un símbolo **sin prompt** no se puede marcar a mano: sólo
/// entra por `select`. Si TODOS los que lo seleccionan caen, él también, aunque nadie
/// dependa de él. Sin esta regla la clausura se dejaba fuera cosas como `ACPI_NHLT`.
///
/// Se exige **al menos un selector**: un símbolo sin prompt y sin selectores entra por otra vía
/// (un `default`), y darlo por muerto mataría media tabla de golpe.
///
/// # Lo que deliberadamente NO incluye
/// Un símbolo con selectores **fuera** de la clausura sigue vivo aunque en el `.config` de hoy
/// quede apagado: sólo se quedó sin usuarios *en esta configuración*. `CRYPTO_LIB_ARC4` se
/// apaga al quitar el wifi, pero vuelve con `PPP_MPPE`. Meterlo en la clausura sería afirmar
/// que el bundle lo mata, y no es cierto.
pub fn closure_off(&self, roots: &[String]) -> BTreeSet<String> {
let mut seen: BTreeSet<String> = BTreeSet::new();
let mut queue: Vec<String> = Vec::new();
@@ -320,14 +368,36 @@ impl KconfigTree {
queue.push(r.clone());
}
}
while let Some(cur) = queue.pop() {
for d in self.dependents_of(&cur) {
if seen.insert(d.clone()) {
queue.push(d.clone());
loop {
while let Some(cur) = queue.pop() {
for d in self.dependents_of(&cur) {
if seen.insert(d.clone()) {
queue.push(d.clone());
}
}
}
// Huérfanos de `select`. Va en un bucle exterior porque matar a uno puede dejar
// huérfano al siguiente, y cada nuevo muerto reabre el cierre por `depends on`.
let mut nuevos: Vec<String> = Vec::new();
for (name, sym) in &self.symbols {
if seen.contains(name) || sym.is_visible() {
continue;
}
let sels = self.selectors_of(name);
if sels.is_empty() || !sels.iter().all(|s| seen.contains(s)) {
continue;
}
nuevos.push(name.clone());
}
if nuevos.is_empty() {
return seen;
}
for n in nuevos {
if seen.insert(n.clone()) {
queue.push(n);
}
}
}
seen
}
/// Cierra la clausura contra sus propias fugas: apaga también a cada `selector` y repite.
@@ -661,6 +731,28 @@ impl Parser<'_> {
}
}
/// `VERSION`/`PATCHLEVEL`/`SUBLEVEL` del `Makefile` de arriba → `"6.16.12"`.
fn read_makefile_version(root: &Path) -> Option<String> {
let text = std::fs::read_to_string(root.join("Makefile")).ok()?;
let mut v = [None, None, None];
for line in text.lines().take(20) {
let Some((k, val)) = line.split_once('=') else {
continue;
};
let val = val.trim();
match k.trim() {
"VERSION" => v[0] = Some(val.to_string()),
"PATCHLEVEL" => v[1] = Some(val.to_string()),
"SUBLEVEL" => v[2] = Some(val.to_string()),
_ => {}
}
}
match v {
[Some(a), Some(b), Some(c)] => Some(format!("{a}.{b}.{c}")),
_ => None,
}
}
/// Une las líneas terminadas en `\` y devuelve `(número de línea original, texto)`.
fn join_continuations(raw: &str) -> Vec<(usize, String)> {
let mut out: Vec<(usize, String)> = Vec::new();
@@ -1071,6 +1163,19 @@ mod tests {
assert_eq!(hard(&e), vec!["X"]);
}
/// `depends on (FB = y) && ARM` es como medio `drivers/video/fbdev` declara que necesita `FB`.
/// Tratar toda comparación como opaca dejaba esos drivers fuera de la clausura.
#[test]
fn comparar_contra_y_o_m_si_es_dura() {
assert_eq!(hard(&parse_expr("(FB = y) && ARM").unwrap()), vec!["ARM", "FB"]);
assert_eq!(hard(&parse_expr("FB = m").unwrap()), vec!["FB"]);
assert_eq!(hard(&parse_expr("FB != n").unwrap()), vec!["FB"]);
// `FB = n` es VERDADERA cuando FB vale n ⇒ no es una dependencia dura.
assert!(hard(&parse_expr("FB = n").unwrap()).is_empty());
// Y el orden de los operandos no cambia nada.
assert_eq!(hard(&parse_expr("y = FB").unwrap()), vec!["FB"]);
}
#[test]
fn constantes_no_son_simbolos() {
let e = parse_expr("A && y").unwrap();
@@ -1152,6 +1257,52 @@ config UNRELATED
);
}
/// El caso `ACPI_NHLT`, encontrado corriendo un `olddefconfig` de verdad (SDD 22 §12).
#[test]
fn el_huerfano_de_select_tambien_muere() {
let d = tempfile::tempdir().unwrap();
std::fs::write(
d.path().join("Kconfig"),
r#"
config WIRELESS
bool "Wireless"
config CFG80211
tristate "cfg80211"
depends on WIRELESS
select HELPER_SOLO_DE_WIFI
select HELPER_COMPARTIDO
config PPP
bool "PPP"
select HELPER_COMPARTIDO
# Sin prompt: NO se puede marcar a mano, sólo entra por select.
config HELPER_SOLO_DE_WIFI
bool
config HELPER_COMPARTIDO
bool
# Sin prompt y sin selectores: entra por otra vía (un default). No se toca.
config HELPER_HUERFANO_DE_NACIMIENTO
bool
"#,
)
.unwrap();
let t = KconfigTree::parse(d.path(), &BTreeMap::new()).unwrap();
let cl = t.closure_off(&["WIRELESS".into()]);
// Su único selector (CFG80211) cae ⇒ no puede volver a encenderse nunca.
assert!(cl.contains("HELPER_SOLO_DE_WIFI"));
// Lo selecciona también PPP, que sigue vivo ⇒ NO es del bundle, aunque en un `.config`
// concreto pueda quedar apagado por falta de usuarios. Meterlo sería afirmar que el bundle
// lo mata, y no es cierto.
assert!(!cl.contains("HELPER_COMPARTIDO"));
// Sin selectores no se deduce nada: darlo por muerto mataría media tabla.
assert!(!cl.contains("HELPER_HUERFANO_DE_NACIMIENTO"));
}
#[test]
fn el_punto_fijo_absorbe_al_selector() {
let (_d, t) = arbol_de_juguete();
+50 -14
View File
@@ -75,9 +75,9 @@ pub struct Plan {
pub knobs: Vec<String>,
/// Símbolos que se emiten, con procedencia.
pub symbols: Vec<PlannedSymbol>,
/// **Diff-back estático**: pedidos que no existen en este árbol. Hoy un `-d` sobre uno de éstos
/// es un no-op que nadie ve — le pasa a `THUNDERBOLT` y a `REISERFS_FS` en las cuatro recetas
/// de kernel del repo.
/// **Diff-back estático**: pedidos que no existen en este árbol. Un `-d` sobre uno de éstos es
/// un no-op que nadie ve. Le pasaba a `THUNDERBOLT` y a `REISERFS_FS` en las cuatro recetas de
/// kernel del repo hasta que se corrigieron (`2602218`), que es cómo se descubrió el patrón.
pub unknown_symbols: Vec<PlannedSymbol>,
/// Perillas `side = "recipe"` seleccionadas: declaradas, no aplicadas.
pub recipe_side: Vec<RecipeKnob>,
@@ -322,8 +322,14 @@ pub struct DiffBack {
pub honored: Vec<String>,
/// Pedidos que el `.config` resultante contradice. **Cada uno es una promesa incumplida.**
pub violated: Vec<Violation>,
/// Pedidos sobre símbolos que el `.config` ni menciona: no existen en esa versión del kernel.
pub absent: Vec<String>,
/// Apagados que el `.config` ni menciona. **Esto es éxito, no fallo**: Kconfig no emite un
/// símbolo cuyas dependencias no se cumplen, así que un `-d` sobre algo que ya cayó con su
/// raíz simplemente no aparece. Contarlo como incumplido —el error que cometí antes de correr
/// un `olddefconfig` de verdad— hace que un plan perfecto se reporte como roto.
pub off_by_dependency: Vec<String>,
/// Símbolos que no existen en este árbol: la bandera no hace nada. Ya se sabía al planear; se
/// repite acá porque quien lee el diff-back no siempre vio el plan.
pub never_existed: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -336,8 +342,10 @@ pub struct Violation {
}
impl DiffBack {
/// Limpio = ninguna promesa incumplida. Los `off_by_dependency` son el resultado buscado y los
/// `never_existed` ya los reportó el plan: ninguno de los dos ensucia el veredicto.
pub fn is_clean(&self) -> bool {
self.violated.is_empty() && self.absent.is_empty()
self.violated.is_empty()
}
}
@@ -347,11 +355,20 @@ pub fn diff_back(plan: &Plan, produced: &super::KernelConfig) -> DiffBack {
let mut d = DiffBack {
honored: Vec::new(),
violated: Vec::new(),
absent: Vec::new(),
off_by_dependency: Vec::new(),
never_existed: Vec::new(),
};
for p in &plan.symbols {
match (p.action, produced.get(&p.symbol)) {
(_, None) => d.absent.push(p.symbol.clone()),
// Ausente + se pedía apagar = está apagado. Kconfig omite lo que no puede existir.
(Action::Disable, None) => d.off_by_dependency.push(p.symbol.clone()),
// Ausente + se pedía encender = NO está. Eso sí es una promesa rota.
(Action::Enable, None) => d.violated.push(Violation {
symbol: p.symbol.clone(),
asked: Action::Enable,
got: "ausente del .config".to_string(),
from: p.from.clone(),
}),
(Action::Enable, Some(v)) if v.is_on() => d.honored.push(p.symbol.clone()),
(Action::Disable, Some(ConfigValue::No)) => d.honored.push(p.symbol.clone()),
(asked, Some(v)) => d.violated.push(Violation {
@@ -362,12 +379,13 @@ pub fn diff_back(plan: &Plan, produced: &super::KernelConfig) -> DiffBack {
}),
}
}
// Los que ya sabíamos inexistentes en el árbol también son promesa incumplida.
for p in &plan.unknown_symbols {
d.absent.push(p.symbol.clone());
}
d.absent.sort();
d.absent.dedup();
d.never_existed = plan
.unknown_symbols
.iter()
.map(|p| p.symbol.clone())
.collect();
d.never_existed.sort();
d.never_existed.dedup();
d
}
@@ -546,6 +564,24 @@ disable = ["REISERFS_FS"]
assert_eq!(d.honored, vec!["WIRELESS"]);
}
/// La lección de correr un `olddefconfig` de verdad (SDD 22 §12): un `-d` sobre un símbolo que
/// el `.config` ni menciona **es el resultado buscado**, no una promesa rota. Contarlo como
/// fallo hacía que un plan perfecto se reportara roto.
#[test]
fn apagar_algo_ausente_es_exito_y_encenderlo_es_fallo() {
let (_d, tree, cat, base) = escenario();
let p = make_plan(&base, "r", &cat, &tree, &["sin-wifi".into()], &["auditoria".into()]).unwrap();
// WIRELESS apagado; WLAN ni aparece (cayó con él); AUDIT tampoco aparece, y ése SÍ se pidió
// encender.
let cfg = super::super::KernelConfig::parse("# CONFIG_WIRELESS is not set\n");
let d = diff_back(&p, &cfg);
assert_eq!(d.honored, vec!["WIRELESS"]);
assert_eq!(d.off_by_dependency, vec!["WLAN"]);
assert_eq!(d.violated.len(), 1);
assert_eq!(d.violated[0].symbol, "AUDIT");
assert!(!d.is_clean(), "falta un símbolo que se pidió ENCENDER");
}
#[test]
fn el_diff_back_limpio_es_limpio() {
let (_d, tree, cat, base) = escenario();
+51
View File
@@ -367,6 +367,57 @@ Un gate global habría rechazado `recipes/linux.toml`, que está sana.
pudieron mapear a ningún símbolo (built-ins de núcleo como `pcieport` o `serial8250`, cuyo nombre de
driver no coincide con el del módulo). Quedan listados como **sin comprobar**, no como aprobados.
## 12. La prueba contra un `olddefconfig` de verdad (2026-08-10)
Hasta acá todo era análisis: la clausura decía qué debía morir, y nadie lo había contrastado con el
resolvedor real. **No hace falta construir un kernel para hacerlo**: lo caro es la fase `compile`
(3560 min); la cadena entera del armador —fragmento → `scripts/config``olddefconfig`
diff-back— vive en `configure` y corre en segundos.
Método: árbol 6.16.12 extraído entero, `make defconfig` como base (que **sí** trae wifi, audio y los
fs encendidos — el config de `linux.toml` no sirve de base porque ya los apaga a mano), el fragmento
del plan, y `olddefconfig`. Ground truth = los símbolos que pasaron de encendidos a apagados.
| refinamiento | clausura | aciertos | **sobra** | falta |
|---|---|---|---|---|
| sólo `depends on` en inversa | 1775 | 64/68 | **0** | 4 |
| + huérfanos de `select` | 1794 | 65/68 | **0** | 3 |
| + comparaciones `X = y` / `X != n` | 1795 | 65/68 | **0** | 3 |
**Sobra 0 en las tres.** El predictor nunca dice que muere algo que sobrevive, que es la única
dirección en la que se puede equivocar sin fabricar un ladrillo.
### Los dos refinamientos que salieron de ahí
- **Huérfanos de `select`.** Un símbolo **sin prompt** no se puede marcar a mano: sólo entra por
`select`. Si todos sus selectores caen, él también, aunque nadie dependa de él. Era el caso de
`ACPI_NHLT`. Se exige ≥1 selector: sin ninguno entra por un `default`, y darlo por muerto mataría
media tabla.
- **`X = y` sí es una dependencia dura.** Medio `drivers/video/fbdev` declara su dependencia de `FB`
como `depends on (FB = y) && ARM`. Tratar toda comparación como opaca dejaba esos drivers fuera.
`X = n` sigue fuera a propósito: con `X` en `n` es **verdadera**. El efecto colateral fue limpiar
el ruido: las fugas `select` sin declarar de `sin-graficos` cayeron de 8+ a 1.
### Los 3 que faltan no son un fallo: son otra pregunta
`CRYPTO_LIB_ARC4`, `REGMAP` y `SYSTEM_DATA_VERIFICATION` se apagaron en ese `.config`, pero tienen
selectores **fuera** de la clausura (`PPP_MPPE`, 111 usuarios más de `REGMAP`, `MODULE_SIG_FORMAT`…).
Se quedaron sin usuarios *en esta configuración*; encendé PPP y `CRYPTO_LIB_ARC4` vuelve.
> **«Inalcanzable» y «apagado ahora» no son lo mismo.** La clausura contesta la primera, que es la
> que define un bundle. Meter los otros tres sería afirmar que el bundle los mata, y no es cierto.
### Y un bug que sólo aparece corriendo el resolvedor
El diff-back contaba como **promesa incumplida** todo símbolo pedido que no apareciera en el
`.config`. Pero Kconfig **no emite** un símbolo cuyas dependencias no se cumplen: un `-d WLAN` cuya
raíz ya cayó simplemente no sale. Con esa cuenta, un plan perfecto se reportaba roto (2 falsos
incumplidos de 16). Ahora se separa: ausente + se pedía apagar = **éxito**; ausente + se pedía
encender = **fallo**. El diff-back de la corrida real sale **14 cumplidos, 0 incumplidos**.
### El guardián que faltaba: las cuatro recetas no son el mismo kernel
`linux` y `linux-metal` van por **6.16.12**; `linux-metal-dual` y `linux-generic` por **7.1.2**.
Planear una contra el árbol de la otra calcularía clausuras sobre símbolos que ahí no existen, y
saldría sin ruido. `KconfigTree` ahora lee su propia versión del `Makefile` de arriba y `plan`
**falla** si no coincide con la de la receta (los diagnósticos sólo avisan).
### 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
+21 -2
View File
@@ -63,8 +63,8 @@ hammer kernel bundles --check # falla si envejeció respecto de este árbol
`--check` es el control de frescura y hace dos cosas:
- **Símbolos idos**: un `-d` sobre un símbolo que ya no existe es un no-op silencioso. Así se
descubrió que las cuatro recetas de kernel del repo apagan `THUNDERBOLT` (hoy `USB4`) y
`REISERFS_FS` (retirado), o sea que no apagan nada.
descubrió que las cuatro recetas de kernel del repo apagaban `THUNDERBOLT` (hoy `USB4`) y
`REISERFS_FS` (retirado), o sea que no apagaban nada. Corregido en `2602218`.
- **Fugas `select` sin declarar**: cada `select` nuevo que entra a un bundle es un símbolo que
upstream agregó y nadie revisó. Es la mitad barata de la curación del delta entre versiones, y
sale de comparar el grafo con el catálogo — sin IA.
@@ -148,6 +148,25 @@ reglas en 3182 ficheros). Lo que no pueda mapear lo lista como **sin comprobar**
como aprobado: un portón que calla lo que no miró no es un portón. Los que suelen quedar fuera son
built-ins de núcleo (`pcieport`, `serial8250`) cuyo nombre de driver no coincide con el del módulo.
## 4.bis Probar un plan SIN construir (30 s en vez de 40 min)
Lo caro es `compile`, no `configure`. La cadena entera del armador vive en `configure`, así que se
puede validar un plan completo sin pagar un build:
```sh
tar -xzf work/tarballs/linux-6.16.12.tar.gz -C work/kernel-test # el árbol ENTERO, 1,7 G
cd work/kernel-test/linux-6.16.12
python3 -c "import json;print(json.load(open('../../kernel-plans/plan.json'))['configure'])" > /tmp/cfg.sh
bash /tmp/cfg.sh # defconfig + fragmento + olddefconfig
hammer kernel diff-back --plan ../../kernel-plans/plan.json --config .config
```
Necesita `gcc`, `make`, `flex`, `bison` y `perl` en el host (`bc` sólo hace falta para compilar).
**Para medir la clausura contra la verdad de campo, la base tiene que ser `defconfig` pelado**, no
el config de `linux.toml`: esa receta ya apaga wifi/audio/fs a mano, así que el lado `disable` del
plan no apagaría nada y la predicción no se pondría a prueba. Es el error que cometí la primera vez.
## 5. Diff-back — después de construir
Sin esto la UI miente: un `-e FOO` cuya dependencia no se cumple se pierde en silencio.
+8 -2
View File
@@ -107,6 +107,7 @@ DRM_AUX_BRIDGE = "lo seleccionan los muxes USB-C (TYPEC_MUX_*): apagarlos romper
DRM_AUX_HPD_BRIDGE = "ídem, TYPEC_QCOM_PMIC / UCSI_HUAWEI_GAOKUN"
BACKLIGHT_CLASS_DEVICE = "lo seleccionan drivers de plataforma (ACPI_CMPC, SAMSUNG_Q10, USB_APPLEDISPLAY)"
DRM_DW_HDMI_I2S_AUDIO = "SND_SOC_RK3288_HDMI_ANALOG, SoC Rockchip: inerte en x86_64"
FB_SYSMEM_HELPERS_DEFERRED = "lo selecciona HID_PICOLCD_FB, una pantallita USB que es un dispositivo HID: decir «no quiero salida gráfica» no debe apagar un HID"
[bundle.contradicted_by]
pci_class = ["0300", "0380"]
@@ -116,7 +117,7 @@ title = "Sólo uso ext4"
help = "Apaga los demás sistemas de ficheros locales. Ojo con el USB de rescate: lo que no está no se monta."
disable = ["XFS_FS", "BTRFS_FS", "F2FS_FS", "JFS_FS", "GFS2_FS", "NTFS3_FS"]
close_leaks = ["NTFS_FS"]
notes = "REISERFS_FS NO va acá: upstream lo retiró y 6.16.12 ya no lo tiene. recipes/linux.toml todavía lo apaga, y ese `-d` es un no-op silencioso."
notes = "REISERFS_FS NO va acá: upstream lo retiró y 6.16.12 ya no lo tiene. Las cuatro recetas lo apagaban en vano hasta `2602218`, que quitó el flag."
[[bundle]]
id = "sin-usb"
@@ -162,7 +163,12 @@ id = "sin-thunderbolt-firewire"
title = "No necesito Thunderbolt ni FireWire"
help = "El símbolo se llama USB4 desde que upstream fundió Thunderbolt y USB4 en un solo driver."
disable = ["USB4", "FIREWIRE"]
notes = "Las CUATRO recetas de kernel apagan `THUNDERBOLT`, que 6.16.12 ya no tiene: ese `-d` es un no-op y el driver sigue entrando por el defconfig."
notes = """
Hasta el 2026-08-10 las cuatro recetas de kernel apagaban `THUNDERBOLT`, que 6.16.12 ya no tiene, y
ese `-d` era un no-op silencioso. Corregido a `USB4` en `2602218`, que lo convierte en un guardián
de verdad: si mañana la base o un `select` lo encienden, ahora sí lo apaga."""
[bundle.accept_leaks]
DRM_DISPLAY_DP_TUNNEL_STATE_DEBUG = "lo selecciona DRM_I915_DEBUG: es una perilla de depuración del driver gráfico, no hardware Thunderbolt"
[bundle.contradicted_by]
driver = ["thunderbolt"]