# 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-`). ```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/-` 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-/`), 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 /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 /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.