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.
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 `hammer_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.
|