Files
takana/docs/22-configurador-kernel.md
Sergio 97ceb72411 takana etapa 5b: los 59 docs de diseño, runbooks y ADR
645 líneas. Los ADR entran porque en este repo SON documentos vivos, no
registros inmutables: el 0013 tiene 5 commits, el 0009 dos. Eso se comprobó
antes de decidir, no se asumió por convención general.

EXCLUIDOS por ser REGISTRO o generado: docs/evidencia/ (6), el HANDOFF de la
noche de KDE (1) y docs/state/ (24, se regenera solo). Reescribir un comando
dentro de una evidencia la falsifica.

Y el ADR 0016 se excluye de todo barrido, con un aviso adentro para el próximo
que barra: habla SOBRE el renombre, así que necesita seguir diciendo 'hammer'.
El barrido se lo llevó puesto y lo dejó titulado 'Renombre del sistema: takana
→ takana'; revertido.

Congelados, verificados uno por uno con controles: /opt/hammer, /var/lib/hammer,
/usr/bin/hammer, /mnt/vvv/hammer, la URL de gitea, hammer-farm.service,
hammer-live-install.sh, BRIEFING-hammer.md, hammerd, hammer-recover y
HAMMER_LIVE.
2026-09-09 19:25:51 +00:00

512 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SDD 22 — Configurador de kernel por niveles: respuesta al handoff de tawasuyu
Escrito 2026-08-07, en respuesta a `tawasuyu/HANDOFF-KERNEL-CONFIG-A-HAMMER.md`.
El handoff avisa de su propio método: *«nada de lo que sigue está verificado contra el código de
takana — si un dato de takana lo contradice, manda takana»*. Este documento es esa verificación. **No
es una aprobación ni un plan de implementación**: contesta las seis preguntas de su §9 con evidencia,
corrige lo que estaba mal, y señala dos objeciones técnicas que cambian el diseño.
**Veredicto corto:** la idea se sostiene y el sustrato está más listo de lo que el handoff supone —
el mecanismo de §6 (*«la app nunca escribe un `.config`»*) **ya es exactamente como funciona hoy**.
Pero hay **un hecho estructural que el handoff no podía conocer y que reordena todo**, y **una
objeción técnica a §2.1** que hay que resolver antes de escribir una línea.
---
## 1. El hecho que reordena todo: **el config ES la identidad del artefacto**
En takana, `Recipe::hash_inputs` es una lista blanca, y **las fases de build entran en ella**. El
config del kernel vive dentro de la fase `configure` de `recipes/linux*.toml`. Por lo tanto:
> **Cambiar un solo símbolo de config cambia el `ArtifactHash` del kernel.**
Esto no es un detalle de implementación: es la bisagra del diseño, y corta en los dos sentidos.
**A favor, y fuerte:** §2.2 (config atestado por huella de hardware) **sale casi gratis**. No hay que
inventar un mecanismo para «atestar un config»: cada config ya *es* un artefacto direccionado por
contenido, reproducible bit a bit y firmable con la maquinaria que existe. Un «config que booteó en
esta huella» es, literalmente, un `ArtifactHash` + una firma. El handoff intuyó que takana tenía el
sustrato; lo tiene más de lo que creía.
**En contra, y también fuerte:** la explosión de builds que §2.2 quiere evitar **no es hipotética,
es aritmética visible en el disco**. Cada variante de config es un artefacto nuevo en el store, y el
store ya crece 24 G por ciclo de trabajo sólo con las variantes que producimos hoy sin querer. Con
N1 (~4060 bundles) el espacio combinatorio es 2⁴⁰; con «perfiles publicados» —que es lo que el
handoff propone en §8— es un puñado. **La restricción de §8 no es una preferencia de diseño: es una
condición de supervivencia del disco**, y conviene decirlo así en la UI.
**Consecuencia práctica que hay que aceptar desde el día uno:** una «perilla» de la UI **no es un
parámetro de runtime, es una edición de receta**. No hay forma de tener un config variable sin
generar recetas. El diseño correcto es que `takana kernel plan` **emita una receta derivada** (con su
propio hash) en vez de fingir que el kernel es un binario parametrizable.
---
## 2. Las seis preguntas del §9, contestadas con evidencia
### P1 — ¿Las recetas de kernel aceptan fragmentos o sólo defconfig completo?
**Ya son fragmentos, y con la forma que §6 prescribe.** `recipes/linux.toml`, fase `configure`:
```
make ARCH=x86_64 defconfig && \
scripts/config -d MODULE_SIG -d DEBUG_INFO_BTF -e SECURITY_LANDLOCK -e AUDIT \
-d WLAN -d WIRELESS -d CFG80211 -d MAC80211 -d RFKILL … && \
make ARCH=x86_64 olddefconfig
```
Base + deltas por símbolo + resolución por el `olddefconfig` **del propio kernel**. La app nunca
escribe un `.config`. La única diferencia con el handoff es el vehículo: `scripts/config -e/-d` en
vez de ficheros de fragmento y `merge_config.sh`. Son equivalentes y `scripts/config` es más fácil de
generar programáticamente. **Media implementación de §6 ya está en producción.**
Falta la otra mitad, que es la que da la UI veraz: **el diff-back**. Hoy nada comprueba que los
símbolos pedidos hayan sobrevivido a `olddefconfig`. Un `-e FOO` cuya dependencia no se cumple se
pierde en silencio — el mismo patrón de fallo que el `license` que caía dentro de `[deps]`.
### P2 — ¿El build es bit-reproducible con config variable?
**Sí, y por construcción.** El config no es un parámetro externo: es parte de la entrada hasheada
(§1). Cada config es su propia receta con su propio hash y su propia garantía de reproducción. No hay
un «kernel» cuya reproducibilidad dependa de congelar el config; hay N kernels, cada uno congelado.
**§2.2 puede prefirmar artefactos, no sólo configs.**
### P3 — ¿Cuánto tarda un kernel completo? 🚨
Declarado en las propias recetas: **`linux` ~35 min, `linux-metal` ~45 min, `linux-generic` ~60 min**
en la máquina de referencia. En la más lenta del parque, más.
**Esto mata la bisección ingenua de §5.** ~6 rebuilds × 45 min = **4,5 horas** de la máquina del
usuario para encontrar un bundle culpable. Nadie espera eso, y menos con la máquina que acaba de no
arrancar. El handoff ya preveía el riesgo («si es caro, tope duro de iteraciones»); la respuesta es
que **es caro**. Ver §4, donde propongo una bisección que no recompila.
### P4 — ¿harkaq puede prestar su motor de clausura?
**El patrón sí; el código no.** `crates/hammer-build/src/harkaq.rs` deriva su política enumerando
**fichero a fichero** los artefactos de las deps declaradas — es una clausura sobre el grafo de
paquetes materializado en el store, con semántica de rutas de filesystem. Kconfig es otro grafo, con
otra semántica y ~15 000 símbolos. Reusar la *idea* («política = clausura, no lista») es correcto y
está validado; reusar la *implementación* no. Hace falta un lector de Kconfig — y ahí está la
objeción del §3.
### P5 — ¿El índice firmado admite un eje más (la huella)?
**Sí, y sin romper nada.** `PackageEntry` (`crates/hammer-core/src/repo.rs`) es un struct serde cuyos
campos opcionales llevan `#[serde(default, skip_serializing_if = ...)]`. Añadir `fingerprint:
Option<String>` es compatible hacia atrás: los índices viejos siguen parseando y los clientes viejos
ignoran el campo. **Es exactamente el truco que hizo pagable el campo `license` hoy mismo.**
### P6 — El prerrequisito de PLAN-KIKIN §4.bis
**Estaba dado por pendiente y está hecho en dos de tres.** Medido:
| receta | `SECURITY_LANDLOCK` | `AUDIT` | `IO_URING` | `BPF_SYSCALL` |
|---|---|---|---|---|
| `linux-metal` | ✅ `-e` | ✅ `-e` | ✅ `-e` | ✅ `-e` |
| `linux-generic` | ✅ `-e` | ✅ `-e` | ✅ `-e` | ✅ `-e` |
| `linux` | ✅ `-e` | ✅ `-e` | ❌ sólo en un comentario | ❌ sólo en un comentario |
⚠️ Y la forma de medirlo importa: `grep IO_URING recipes/linux.toml` **da positivo** porque la palabra
aparece en un comentario. Hay que mirar los `-e`/`-d` **dentro del bloque `configure`**. Es el mismo
error que hoy infló el conteo de licencias (`grep -l license` contaba comentarios y nombres de
paquete) y el que hace mentir a la regla del `.a` no-PIC. **En este repo, `grep` sobre ficheros con
comentarios densos no es una medición.**
⇒ Queda pendiente sólo `linux`, que es el kernel de QEMU/serial.
---
## 3. Objeción técnica a §2.1: «clausura» sobre Kconfig no está bien definida todavía
La propuesta —«todo lo alcanzable desde `CONFIG_WIRELESS` menos lo alcanzable desde lo que sí
quiero»— asume un grafo con aristas de un solo tipo. Kconfig tiene **al menos tres, con semánticas
incompatibles**:
- `depends on` — arista hacia arriba: *no puedo existir sin X*.
- `select` — arista hacia abajo y **forzada**: *si yo entro, X entra*, ignorando los `depends on` de
X. Es la fuente clásica de configs inválidos.
- `imply` — sugerencia blanda.
«Alcanzable» por `depends on` y «alcanzable» por `select` dan conjuntos distintos y **ninguno de los
dos es la respuesta**. Un driver de otro subsistema puede `select` un helper de wireless sin ser
wireless: cae dentro de la clausura ingenua y apagarlo rompe algo que el usuario sí quería. Y al
revés, `select` puede meter símbolos wireless desde fuera de la clausura.
**Esto no invalida §2.1 — lo convierte en la parte con contenido técnico real**, tal como el propio
handoff dice en su §10. Pero hay que definir la semántica de arista antes de escribir el predicado, y
la definición hay que **validarla contra algo**.
### Y tenemos contra qué validarla, hoy y gratis
El config actual de `linux.toml` **ya es un juego de bundles N1 escrito a mano**. Literalmente:
```
-d WLAN -d WIRELESS -d CFG80211 -d MAC80211 -d RFKILL ← el bundle «no necesito wifi»
-d SOUND -d SND ← «no necesito audio»
-d DRM -d DRM_I915 -d DRM_AMDGPU -d DRM_NOUVEAU -d FB ← «sin gráficos»
-d XFS_FS -d BTRFS_FS -d F2FS_FS -d JFS_FS -d REISERFS_FS ← «sólo ext4»
```
Es un corpus de verdad-de-campo: alguien decidió a mano qué símbolos componen «no necesito wifi», y
ese kernel **arranca**. La prueba de §2.1 es directa y barata: *¿la clausura calculada de
`CONFIG_WIRELESS` reproduce ese conjunto?* Si sobra o falta mucho, la semántica de arista está mal y
nos enteramos en una tarde, sin compilar nada. **Refuerza el §10 del handoff: eso se hace antes que
nada, junto al modo reversa.**
---
## 4. Lo que agrego: bisección **sin recompilar**
Dado P3 (3560 min por kernel), la bisección de §5 cuesta horas. Pero la mayoría de los bundles N1
son **drivers**, y un driver puede ser `=m` en vez de `=y`.
Propuesta: **un kernel «superset» con todo lo bisecable como módulo**. Los bundles N1 de tipo
hardware no se resuelven entonces recompilando, sino con una **lista de bloqueo de módulos en la
línea de arranque**. La bisección deja de ser 6 rebuilds (4,5 h) y pasa a ser **6 reinicios**
(minutos), sobre un kernel que ya está en el árbol de generaciones.
Consecuencias:
- El «kernel a medida» pasa a ser una optimización de tamaño/arranque, no un requisito de
funcionalidad ⇒ el usuario puede probar bundles antes de pagar un build.
- Encaja con el árbol de generaciones y el rollback E4, que ya existen.
- **Limitación honesta:** no cubre lo que debe ser `=y` (LSM, preempción, unwinder, `-march`, LTO,
o sea buena parte de N2). Para esos sí hay rebuild y sí hay que topar iteraciones.
- **Y un coste que hay que decir:** el superset es exactamente el kernel gordo que el nivel 1 quiere
evitar. Es una herramienta de diagnóstico, no el kernel de todos los días.
## 5. Lo que agrego: el gate de no-regresión (§6) debe ser **por objetivo**, no global
El handoff lo llama la mejor relación valor/esfuerzo y estoy de acuerdo. Pero el config actual de
`linux.toml` apaga `USB_SUPPORT`, `HID`, `HID_SUPPORT`, `INPUT_MOUSE`, `INPUT_TOUCHSCREEN`. Ese
kernel **no puede recibir teclado en metal** — y es correcto, porque es el kernel de QEMU con consola
serie. Un gate global «todo dispositivo en uso debe seguir teniendo driver» **rechazaría una receta
sana**.
⇒ El gate necesita el objetivo declarado (`qemu-serial` / `metal` / `servidor`) y una noción de
«dispositivos en uso **de la máquina destino**», que no siempre es la máquina donde se construye.
Sigue siendo la propuesta más barata de las nueve; sólo que el portón se pone en el sitio correcto.
## 6. Dos huecos del handoff que conviene cerrar antes
**«Booteó» no es una aserción suficiente para atestar (§2.2).** Un kernel puede arrancar y dejarte
sin red, sin wifi o sin teclado — que es justo el desastre que §6 quiere evitar. Si la atestación
dice sólo «booteó en esta huella», estamos firmando la clase de fallo que más duele. La atestación
debería llevar **qué se comprobó**: qué dispositivos de la huella quedaron con driver bindeado
después de arrancar. Es el mismo dato que ya calcula el gate de §6 ⇒ una implementación, dos usos.
**El catálogo de bundles necesita las tres etiquetas de nivel, no dos.** El handoff avisa en §5 que
«la mitad de N2 no es Kconfig sino variables de receta». Con §1 en la mano eso es más grave de lo que
parece: las perillas de receta (LTO, `-march`, `zig_version`) **cambian el hash igual que los
símbolos**, pero se aplican en otra fase y fallan de otra forma. La UI tiene que saber de qué lado
cae cada perilla o prometerá diffs que no puede explicar.
---
## 7. Qué contesto al reparto (§7)
De acuerdo con el reparto tal como está. Dos precisiones desde este lado:
- El contrato JSON: de acuerdo, y el precedente que cita (`/run/hammer/boot-graph.json`) es el
correcto. Añadiría que el JSON de `takana kernel plan` **debe incluir el `ArtifactHash` resultante**,
porque por §1 el plan *determina* el artefacto: la UI puede decir «esto ya está construido y
firmado» sin construir nada.
- Los verbos propuestos me parecen bien salvo `takana kernel build --plan`, que esconde dos cosas muy
distintas (construir, y dar de alta en el árbol de generaciones). Separarlos deja que la sonda en
VM de §4 se meta entre medio como un paso propio y auditable.
## 8. Orden que propongo (modifica el §10 del handoff)
1. **Modo reversa (#9)** — tal cual lo propone: cero riesgo, valida el catálogo contra un kernel real.
2. **Validar la clausura contra el config actual** (§3 de este documento) — sale gratis, usa un corpus
que ya existe, y decide si §2.1 es viable. **Va antes que el gate**, porque si la clausura no
reproduce los bundles hechos a mano, el diseño de N1 cambia.
3. **Gate de no-regresión (#6), por objetivo** (§5 de este documento).
4. **Diff-back** (la mitad que falta de §6 del handoff): barato, y sin él la UI miente.
5. Recién después, #1 (clausuras) con la semántica ya decidida.
**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
`takana 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` (`takana 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?*
```
takana 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.
## 10. Pasos 1 y 4 hechos: modo reversa, plan y diff-back
**Modo reversa** (`takana kernel probe`) sobre gioser, con el catálogo `docs/state/kernel-bundles.toml`
(15 bundles N1, 8 perillas N2): 10 551 símbolos declarados, 20 dispositivos PCI, 38 drivers
bindeados, **7 bundles con capacidad que este hardware no usa**. Y avisa de lo que el propio caso
destapó: el config vivo es de la serie 7.1 y el catálogo se revisó contra la 6.16 ⇒ las clausuras
son aproximadas. Se dice en vez de callarlo.
**Plan** (`takana kernel plan`): emite una **receta derivada**, como manda §1. Medido sobre
`recipes/linux.toml` con `sin-wifi` + `sin-audio` + `solo-ext4` + `jaula-y-eio-moderna`:
| | |
|---|---|
| banderas emitidas | **16** |
| símbolos que caen con su clausura | **1775** |
| `ArtifactHash` de la base | `b3:cb926743…` |
| `ArtifactHash` de la derivada | `b3:47a52b2e…` |
Dos hashes distintos para el mismo código fuente: **el config es la identidad**, y ahora se ve. El
plan lleva su hash dentro ⇒ la UI puede decir «esto ya está construido y firmado» sin construir.
Tres detalles del plan que no eran obvios:
- **Se emiten raíces, no clausuras.** 16 banderas, no 1775 líneas. La clausura la calcula el
`olddefconfig` del propio kernel; takana la sabe sólo para poder explicarla.
- **La fase derivada AÑADE una segunda ronda** (`… && scripts/config … && make olddefconfig`) en vez
de reescribir la base. No hay que parsear el shell de nadie y la base sigue siendo literalmente la
de siempre en el diff.
- **Los conflictos se rechazan, no se ordenan.** Dos selecciones que se contradicen sobre un símbolo
darían una respuesta plausible y arbitraria según el orden de aparición. Y hay un segundo conflicto
que el símbolo solo no delata: encender algo que **cae dentro de la clausura** de lo que otro
bundle apaga — `olddefconfig` lo descartaría sin decir nada.
**Diff-back** (`takana kernel diff-back --plan … --config …`): la mitad que faltaba del §6 del
handoff. Clasifica cada símbolo pedido en cumplido / **incumplido** (el `.config` dice otra cosa) /
ausente (el kernel ni lo menciona: la bandera fue un no-op), con la procedencia de quién lo pidió, y
sale distinto de cero si el config no honra el plan.
## 11. Paso 3 hecho: el gate de no-regresión, por objetivo
La pieza que faltaba no era el gate sino el **mapa driver → símbolo**: el kernel sabe qué driver
tiene bindeado cada dispositivo, pero no de qué `CONFIG_*` salió. Esa relación sólo existe en los
Makefiles de kbuild (`obj-$(CONFIG_SND_HDA_INTEL) += snd-hda-intel.o`). Leídas **15 789 reglas en
3182 Makefiles**.
Dos trampas de nombres que, sin resolver, harían que el gate no encontrara nada y dijera que todo
está bien — el peor resultado posible para un portón:
- el módulo cargado usa `_` donde el fichero usa `-` (`snd-hda-intel.o``snd_hda_intel`);
- un módulo puede salir de **varios** símbolos, y sobrevive si sobrevive cualquiera.
**La prueba de que el portón está en el sitio correcto**, con el mismo plan (`sin-usb` +
`sin-entrada-humana` + `sin-graficos` + `sin-wifi`) y el hardware real de gioser:
| objetivo | veredicto | detalle |
|---|---|---|
| `qemu-serial` | ✓ pasa | 5 pérdidas AUTORIZADAS (`hid-generic`, `psmouse`, `usbhid`, `virtio_gpu`, `xhci_hcd`) |
| `metal-escritorio` | ✗ bloquea | las mismas 5, como REGRESIONES, cada una con el bundle culpable |
Un gate global habría rechazado `recipes/linux.toml`, que está sana.
**Y lo que el gate no puede comprobar, lo dice.** De los 38 drivers bindeados en gioser, 15 no se
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).
## 13. Caso de punta a punta: un kernel a medida de gioser (2026-08-11)
La primera vez que el armador se usa para lo que existe. Máquina: Hetzner vServer, AMD EPYC-Rome,
4 vCPU, 20 dispositivos PCI, 38 drivers bindeados.
**Base: `linux-metal`, no `linux`.** El `probe` decide: `linux` apaga USB, HID e INPUT a propósito
(es el kernel de QEMU con consola serie) y gioser tiene `xhci_hcd`, `usbhid` e `i8042` bindeados;
`linux-metal` conserva los tres, más virtio, ext4, vfat y EFI. Elegir la base es la primera decisión
y el `probe` la contesta con datos, no con intuición.
**Selección:** 13 bundles y 3 perillas → **34 banderas**, clausura de 3054 símbolos.
| | símbolos encendidos |
|---|---|
| `linux-metal` tal cual | 1805 |
| derivada para gioser | **1647** (158) |
Segunda validación de la clausura contra el `olddefconfig` real, ahora sobre otra base y con 13
bundles: **154 aciertos, SOBRA 0**, faltan 18 (todos helpers sin prompt del tipo `DRM_DISPLAY_*`,
`CRYPTO_LIB_ARC4` — la misma clase «apagado ahora ≠ inalcanzable» del §12).
### Lo que sólo se ve midiendo esta máquina
El gate contra el `.config` producido, con `/proc/config.gz` de referente, encontró **cuatro
pérdidas que ningún bundle causaba**: `aer`, `iTCO_wdt`, `lpc_ich` y `pcspkr` están encendidos en el
kernel que corre y **`linux-metal` no los enciende nunca**. No era una regresión del plan: era un
hueco de la receta base, invisible para el gate del §11 por construcción.
Lo mismo con `VIRTIO_BALLOON` y `HW_RANDOM_VIRTIO`: **ninguna receta de kernel del repo los
enciende** y en gioser los dos están bindeados. Un kernel de takana arranca igual en esta máquina,
pero la VM se queda sin globo de memoria y sin la entropía del anfitrión.
⇒ Tres entradas nuevas de catálogo, todas nacidas de medir: bundle `sin-gpu-intel` (i915 es de los
drivers más grandes del kernel y en una VM con virtio-gpu no sirve) y perillas `invitado-virtio` y
`plataforma-pc`. Con ellas el gate pasa: **✓ ningún dispositivo en uso se queda sin driver.**
### El artefacto: construido, sellado y verificado
```
b3:8ffd57109a2f7102c1bbbbc91cf0e729047312eee1a4e3fdb8b178680cc94264-linux-gioser
```
| | `linux-metal` | derivada gioser |
|---|---|---|
| `bzImage` | 14,5 MB | **11,1 MB** (23%) |
| símbolos encendidos | 1805 | 1647 |
- **diff-back contra el artefacto: 35 cumplidos, 0 incumplidos.**
- **gate contra el artefacto** (referente `/proc/config.gz`): ✓ ningún dispositivo en uso se queda
sin driver.
### La prueba de 30 s predijo el build de 45 min
El `.config` del artefacto y el de la prueba en seco (§4.bis del runbook) difieren en **14 líneas, y
ni una es funcional**: son las siete versiones del toolchain que el kernel graba en su propio
config.
```
< CONFIG_CC_VERSION_TEXT="gcc (Alpine 15.2.0) 15.2.0" ← el lab anclado
> CONFIG_CC_VERSION_TEXT="gcc (GCC) 16.1.1 20260725" ← el host
… GCC_VERSION, AS_VERSION, LD_VERSION, RUSTC_VERSION, RUSTC_LLVM_VERSION, PAHOLE_VERSION
```
Cero símbolos de diferencia. ⇒ **la prueba barata es un sustituto fiel del build** para todo lo que
el armador decide.
Y de regalo, la mecánica exacta de por qué el lab tiene que estar en `hash_inputs`: **el kernel
graba la versión de su compilador DENTRO del `.config`**, y el `.config` va dentro del artefacto. No
es que el lab «influya» en el resultado — es que **el lab está literalmente en el contenido
sellado**. (`PAHOLE_VERSION=0` en el artefacto confirma además lo que dice la cabecera de
`linux.toml`: el toolchain del lab no trae pahole.)
### El paralelismo entra en `hash_inputs`, y hay que saberlo
gioser estaba al borde del OOM (swap 3611/4095, 12 `oom-kill` previos) **y hospeda gitea**, en una
máquina marcada como fija y sin backup. Bajar el build a `-j1` es lo prudente — y cambia el hash:
```
-j"$(nproc)" b3:10a60ceb…
-j1 b3:8ffd5710…
```
El paralelismo **no cambia un byte del `bzImage`**, pero el texto de la fase `compile` está en
`hash_inputs`. Por eso el `$(nproc)` de las recetas base no es estilo: mantiene el texto
**independiente de la máquina** y evita bifurcar la identidad del artefacto por el número de cores.
**Si hay que capar el paralelismo, va FUERA de la receta** (entorno, cgroup, `nice`); editando la
fase, cada máquina lenta se fabrica su propio hash. Es el reverso exacto del caso `USB4`: allá
cambió el texto y no el `.config`; acá cambia el texto y no el binario.
### 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**.