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.
512 lines
30 KiB
Markdown
512 lines
30 KiB
Markdown
# 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 (~40–60 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 (35–60 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`
|
||
(35–60 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**.
|