250 líneas de comentario en 151 scripts. Control verificado: el diff no toca NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa. El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces: las tres eran el MOTD que el script escribe DENTRO de la imagen construida — texto del producto, no comentario del script. Se cambiaron aparte y a propósito, que es rebranding, no limpieza. Y el hallazgo caro: casaba contra , que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate. La etapa 4 lo movió a y el script quedó casando NADA. No fallaba: imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja. Además 14 rutas de módulo en docs, que el barrido anterior no tocó porque no es frontera de palabra.
265 lines
13 KiB
Markdown
265 lines
13 KiB
Markdown
# Runbook — el armador de kernel (`takana kernel`)
|
||
|
||
Implementa `docs/22-configurador-kernel.md` (SDD 22), que contesta
|
||
`tawasuyu/HANDOFF-KERNEL-CONFIG-A-HAMMER.md`.
|
||
|
||
**La regla que no se cruza:** takana **lee** el grafo de Kconfig, no lo resuelve. El `.config` lo
|
||
sigue produciendo el `olddefconfig` del propio kernel. Lo que la app emite son **fragmentos**
|
||
(`scripts/config -e/-d`) y una **receta derivada**.
|
||
|
||
**El hecho que ordena todo:** las fases de build entran en `Recipe::hash_inputs` y el config vive en
|
||
la fase `configure` ⇒ **el config ES la identidad del artefacto**. Cambiar un símbolo cambia el
|
||
`ArtifactHash`. Por eso una perilla de UI no es un parámetro de runtime: es una edición de receta.
|
||
|
||
---
|
||
|
||
## 0. Preparar el árbol de fuentes
|
||
|
||
Los comandos necesitan un árbol de fuentes del kernel, del que sólo se leen los `Kconfig*` y los
|
||
`Makefile*`. No hace falta extraer el tarball entero (1,5 G): con estos dos juegos bastan 38 M.
|
||
|
||
```sh
|
||
curl -sSLo work/tarballs/linux-6.16.12.tar.gz \
|
||
https://mirrors.edge.kernel.org/pub/linux/kernel/v6.x/linux-6.16.12.tar.gz
|
||
mkdir -p work/kconfig-6.16.12
|
||
tar -xzf work/tarballs/linux-6.16.12.tar.gz -C work/kconfig-6.16.12 --strip-components=1 \
|
||
--wildcards '*/Kconfig*' '*/Makefile' '*/Makefile.*' 'linux-6.16.12/arch/x86/configs/*'
|
||
export TAKANA_KCONFIG_ROOT=work/kconfig-6.16.12
|
||
```
|
||
|
||
`work/` está en `.gitignore`. El `sha256` del tarball es el mismo que pinea `recipes/linux.toml`.
|
||
|
||
Control de salud del lector — **si esto no da 0 avisos, no confíes en lo que sigue**:
|
||
|
||
```sh
|
||
takana kernel stats
|
||
# ficheros 1646 · símbolos 18212 (15169 visibles) · select 15165 · imply 445 · avisos 0
|
||
```
|
||
|
||
## 1. Modo reversa — mirar el kernel que ya corre
|
||
|
||
Cero build, cero riesgo. Lee `/proc/config.gz` (o `/boot/config-<release>`).
|
||
|
||
```sh
|
||
takana kernel probe
|
||
```
|
||
|
||
Contesta tres cosas: cuánto de cada bundle ya rige, qué capacidad carga este kernel que esta máquina
|
||
no usa, y dónde el hardware **contradice** a un bundle aplicado. La detección va en un solo sentido:
|
||
sirve para contradecir, **nunca** para podar sola — no se puede detectar el dock que se enchufa el
|
||
mes que viene ni el fs del USB de rescate.
|
||
|
||
Si el config vivo y el catálogo son de series distintas, lo avisa: las clausuras salen aproximadas.
|
||
|
||
## 2. El catálogo
|
||
|
||
`docs/state/kernel-bundles.toml`. Un bundle **no es una lista de símbolos**: es una raíz y su
|
||
clausura. `disable = ["WIRELESS"]` son ~400 `CONFIG_*` que takana calcula.
|
||
|
||
```sh
|
||
takana kernel bundles # el catálogo con las clausuras resueltas
|
||
takana 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 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.
|
||
|
||
Para decidir qué hacer con una fuga:
|
||
|
||
```sh
|
||
takana kernel closure WIRELESS --fixpoint
|
||
```
|
||
|
||
El punto fijo **reporta el precio, no lo aplica**. Cerrar «sin audio» exige apagar
|
||
`DRM_I915`/`NOUVEAU`/`AMD_DC`, que hacen `select` del códec HDMI: en un escritorio eso es una
|
||
decisión, no una limpieza. Se resuelve una vez, con `close_leaks` o `accept_leaks`.
|
||
|
||
## 3. Planear
|
||
|
||
```sh
|
||
takana kernel plan --recipe recipes/linux.toml \
|
||
--bundle sin-wifi --bundle sin-audio --bundle solo-ext4 \
|
||
--knob jaula-y-eio-moderna \
|
||
--out work/kernel-plans/plan.json \
|
||
--recipe-out work/kernel-plans/linux-derivada.toml
|
||
```
|
||
|
||
Emite **raíces**, no clausuras: 16 banderas, no 1775 líneas. Y lleva el `ArtifactHash` de la receta
|
||
derivada dentro del JSON, así que la UI puede decir «esto ya está construido y firmado» **sin
|
||
construir**.
|
||
|
||
⚠ Para construir la receta derivada hay que dejarla **junto a la receta base**, o sus `deps.build`
|
||
no resuelven. `plan` no escribe en `recipes/` por su cuenta: ese directorio es el corpus compartido.
|
||
|
||
⚠ **Y al construirla, `takana build` va SIEMPRE bajo el lock compartido:**
|
||
|
||
```sh
|
||
flock work/.farm-build.lock ./target/release/takana --store ./store build recipes/linux-derivada.toml
|
||
```
|
||
|
||
`takana build` comparte `work/sources/<dep>-<sha>` entre todas las recetas. Dos builds simultáneos
|
||
que compartan una dependencia se pisan: uno hace fetch y borra el árbol mientras el otro lo usa, y
|
||
**el árbol queda roto para siempre** — reintentar no lo arregla. Medido a escala: al invalidar
|
||
`libdrm`, ~93 de 205 recetas KDE murieron con «`/src/.zwrap/cc` is not a full path to an existing
|
||
compiler tool», que es el wrapper de zig que la receta deja **en el árbol de fuente**, barrido por el
|
||
fetch concurrente de otra. Es el ADR 0012, todavía sin decidir.
|
||
|
||
Es el mismo fichero de lock que toman `scripts/farm/farm-worker-loop.sh` y `campana-deuda.sh`, así
|
||
que con esto quedan serializados los tres. Deliberadamente **no** está dentro de `takana build`: los
|
||
scripts de la granja ya lo toman por fuera y takana se bloquearía contra ellos.
|
||
|
||
Nada más del armador toca el store: `plan` calcula el `ArtifactHash` con `takana_build::artifact_hash`,
|
||
que es cómputo puro sobre las recetas, y `probe`/`gate`/`bundles`/`closure` sólo leen.
|
||
|
||
**Si el build muere con «no encuentro el ejecutable zig en …», el zig está bien: falta ESA versión.**
|
||
takana lo busca por **directorio versionado** (`.dev-fs/tools/zig-x86_64-linux-<ver>/`), no por el
|
||
symlink `tools/zig` ni por el `PATH`. `linux.toml` no pinea `zig_version`, pero tres de sus
|
||
`deps.build` sí — `flex`, `openssl` y `elfutils`, las tres a **0.13.0** — y la receta derivada las
|
||
hereda enteras.
|
||
|
||
## 4. El gate de no-regresión — **por objetivo**
|
||
|
||
La regla es «todo dispositivo en uso debe seguir teniendo driver». Aplicada global rechazaría
|
||
`recipes/linux.toml`, que apaga USB, HID e INPUT **a propósito** por ser el kernel de QEMU con
|
||
consola serie. Por eso el gate **no corre sin `--objective`**.
|
||
|
||
En la máquina **destino** (que no tiene por qué ser la de build):
|
||
|
||
```sh
|
||
takana kernel hw --out work/kernel-plans/hw.json
|
||
```
|
||
|
||
En la de build:
|
||
|
||
```sh
|
||
takana kernel gate --plan work/kernel-plans/plan.json \
|
||
--objective qemu-serial --devices work/kernel-plans/hw.json
|
||
```
|
||
|
||
El mismo plan **pasa** en `qemu-serial` y **bloquea** en `metal-escritorio`. Ése es todo el punto.
|
||
|
||
El gate mapea driver → símbolo leyendo las reglas `obj-$(CONFIG_X) += y.o` de los Makefiles (15 789
|
||
reglas en 3182 ficheros). Lo que no pueda mapear lo lista como **sin comprobar** y **no** lo cuenta
|
||
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
|
||
takana 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.
|
||
|
||
## 4.ter Caso resuelto: el kernel a medida de gioser
|
||
|
||
`docs/state/kernel-plans/` guarda el plan y la receta derivada del primer kernel que salió del
|
||
armador de punta a punta. Reproducirlo:
|
||
|
||
```sh
|
||
takana kernel plan --recipe recipes/linux-metal.toml \
|
||
--bundle sin-wifi --bundle sin-bluetooth --bundle sin-nfc --bundle sin-radios \
|
||
--bundle sin-audio --bundle sin-camaras-tv --bundle sin-infiniband --bundle sin-bus-industrial \
|
||
--bundle sin-thunderbolt-firewire --bundle sin-lector-de-tarjetas --bundle sin-virtualizacion \
|
||
--bundle sin-gpu-intel --bundle solo-ext4 \
|
||
--knob invitado-virtio --knob plataforma-pc --knob jaula-y-eio-moderna \
|
||
--out plan.json --recipe-out linux-gioser.toml
|
||
```
|
||
|
||
Resultado: `bzImage` de **11,1 MB** contra los 14,5 MB de `linux-metal` (−23%), 1647 símbolos
|
||
encendidos contra 1805, diff-back limpio y gate en verde.
|
||
|
||
⚠ **La receta derivada NO se deja en `recipes/`.** `build-state.py` y yupana barren `recipes/` y
|
||
`recipes/incoming-*/`, así que dejarla ahí la mete en el grafo compartido como deuda. Para
|
||
construir, copiala a `recipes/incoming-kernel/` **temporalmente** (sus `deps.build` resuelven contra
|
||
el catálogo padre) y sacala después. El artefacto sellado sobrevive; la receta se regenera con el
|
||
comando de arriba.
|
||
|
||
⚠ La copia guardada **ya no lleva** el `-j1` de la primera vuelta: al pagar SDD 25 H1 (MEMCG+PSI)
|
||
hubo que regenerarla contra la base nueva, y ese re-hasheo obligado fue el momento barato para
|
||
soltar la concesión y volver al `-j"$(nproc)"` que emite `plan` — capar el paralelismo en la receta
|
||
cambia el `ArtifactHash` aunque el binario sea idéntico (SDD 22 §13), y el throttling va por
|
||
entorno. Historia de los hashes: `b3:8ffd5710…` era la variante `-j1` sin MEMCG; la vigente es
|
||
`b3:b026fe14…`.
|
||
|
||
⚠ **Regenerar la derivada NO alcanza para que el MEMCG de la base llegue acá**: la receta derivada
|
||
**inlinea** la fase `configure` de su base. Cambiar `recipes/linux-metal.toml` y no regenerar deja
|
||
la derivada con el configure viejo, y el gate lo ve (el sellado sale como vigente sin la
|
||
capacidad).
|
||
|
||
## 5. Diff-back — después de construir
|
||
|
||
Sin esto la UI miente: un `-e FOO` cuya dependencia no se cumple se pierde en silencio.
|
||
|
||
```sh
|
||
takana kernel diff-back --plan work/kernel-plans/plan.json \
|
||
--config <artefacto>/boot/config-6.16.12
|
||
```
|
||
|
||
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ó. Sale
|
||
distinto de cero si el config no honra el plan.
|
||
|
||
## 6. El contrato de capacidades — antes de dar un kernel por bueno
|
||
|
||
`diff-back` contesta «¿el `.config` honra el PLAN?». Falta la otra mitad: **«¿el kernel trae lo que
|
||
el userland usa por nombre?»**. Son preguntas distintas — un kernel puede honrar su plan al pie de
|
||
la letra y no tener `CONFIG_MEMCG`, que nunca estuvo en ningún plan porque nadie lo pidió (SDD 25
|
||
§4: por eso `memory.max` no existe y arje escribe al vacío dejando sólo un `warn!`).
|
||
|
||
```sh
|
||
takana --store ./store kernel contract --sealed # todos los kernels sellados del store
|
||
takana kernel contract --config <art>/boot/config-7.1.2 # uno suelto (el perfil sale del nombre)
|
||
takana kernel contract --profile anfitrion-cards # el kernel VIVO de esta máquina
|
||
takana kernel contract --list # qué promete takana, y quién lo consume
|
||
```
|
||
|
||
Sale distinto de cero si falta una capacidad **exigida por el perfil**. Tres cosas que conviene
|
||
saber antes de leer su salida:
|
||
|
||
- **Corre por perfil.** `docs/state/kernel-contract.toml` mapea artefacto → perfil. `linux` es
|
||
`qemu-serial` (no hospeda Cards, y su hash es el baseline del selfhost-verify); los demás son
|
||
`anfitrion-cards`. Un artefacto **sin fila** sale **SIN COMPROBAR**, que no es lo mismo que
|
||
aprobado.
|
||
- **Se mira contra el `.config` producido**, nunca contra la receta: entre el `scripts/config -e X`
|
||
y el `.config` está `olddefconfig`.
|
||
- **Hoy sale rojo a propósito**: 2 de 11 configs sellados cumplen. Es H1 de SDD 25 sin pagar
|
||
(`-e MEMCG -e PSI` re-hashea los kernels ⇒ va en tanda). Cuando se pague, este comando es la
|
||
prueba.
|
||
|
||
**Una capacidad nueva entra al contrato con consumidor —ruta y símbolo— o no entra.** «Estaría
|
||
bueno tenerlo» pertenece a un bundle del catálogo, no acá.
|
||
|
||
---
|
||
|
||
## Lo que todavía NO está
|
||
|
||
- **Sonda de arranque en VM** (#4 del handoff): construir → bootear en QEMU con el perfil de la
|
||
máquina destino → recién ahí dar de alta en el árbol de generaciones.
|
||
- **Atestación por huella** (#3): `takana kernel hw` ya da la huella y el plan ya da el
|
||
`ArtifactHash`; falta el eje `fingerprint` en `PackageEntry` y el quórum de `minga atestar`. Y
|
||
ojo: «booteó» no basta para atestar — la atestación tiene que llevar **qué se comprobó**, que es
|
||
el mismo dato que calcula el gate.
|
||
- **Bisección** (#5): un kernel completo tarda 35–60 min ⇒ la bisección ingenua son ~4,5 h. La salida
|
||
propuesta (SDD 22 §4) es un kernel «superset» con lo bisecable como módulo: 6 **reinicios** en vez
|
||
de 6 rebuilds.
|
||
- **Curación del delta con modelo** (#2): hoy `--check` marca lo no clasificado; falta que alguien
|
||
proponga a qué bundle va cada símbolo nuevo.
|
||
- Aplicar las perillas `side = "recipe"`: hoy se **declaran** y no se aplican.
|