gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1488Shell 182Rust 107Markdown 963619 Definitions

⎇ 0ecabf428a387c4d9e7d1bc0575090bc02d46a65 docs/plan-que-sigue-tras-busybox.md Markdown · 347 lines · 19.4 KB History raw

plan: con qué conviene hacer lo de busybox — la imagen lleva el toolchain adentro 0ecabf428 · Sergio ·

1# Plan — con qué conviene hacer lo de busybox, MEDIDO
2
3**Fecha:** 2026-09-22 · **Pregunta:** «botado busybox, ¿con qué conviene hacer lo mismo? ¿Hay que
4rusterizar o soberanizar algo vital, hay opciones modernas a cosas tradicionales, o hay una
5arquitectura mejor?»
6
7**Instrumento** (todo reproducible, nada escrito a mano):
8
9```sh
10# grafo y clases: los 950 nodos con su clase, hash, perfiles y dependientes
11python3 -c 'import json;json.load(open("docs/state/build-state.json"))'
12# aristas y membresía: la MISMA función que usan el grafo, la hidratación y el repo firmado
13python3 -c 'import sys;sys.path.insert(0,"scripts");import yupana;yupana.cargar_colas()'
14# bytes: tamaño real de cada artefacto sellado
15cd /store && du -sk --apparent-size *          # 1.435 artefactos, 59,8 GiB
16# runtime real: quién pide qué SONAME
17readelf -d <artefacto>/usr/bin/<bin>
18```
19
20## El hallazgo que ordena todo, y no es una pieza de userland
21
22> **La clausura que define una imagen es `build ∪ runtime`. Entonces cada imagen de takana lleva
23> adentro la cadena de compilación: `llvm21`, `go`, `python3`, `binutils`, `cmake`, `perl`, `nasm`,
24> `bison`, `flex`, `m4`, `meson`, `wasi-sdk`, `nodejs`.**
25
26Es **exactamente la forma del caso busybox**, y por eso vale la pena verlo dos veces: `busybox`
27nunca estuvo declarado en `targets.toml` —llegaba a los 7 perfiles por la clausura, porque 24
28recetas lo pedían en `[deps] build`—, y sacarlo no costó escribir una línea de userland (paso 6 de
29`plan-botar-busybox.md`). Lo que ese paso arregló **para un nombre**, esto lo arregla **para la
30clase entera**.
31
32La definición está en `scripts/yupana.py:98` (`_deps`) y en `scripts/build-state.py:128`, y en las
33dos hay un comentario que explica por qué es la unión: `firefox` declaraba `runtime = ["gcc-libs"]`,
34nadie leía ese campo, y la imagen se armaba sin `libstdc++.so.6`. **El arreglo fue correcto y la
35unión es lo que hay que afinar, no lo que hay que deshacer** — ver el riesgo más abajo.
36
37### Lo que cuesta, en nodos y en bytes
38
39Clausura de hoy contra «raíces + aristas de runtime», medido sobre los artefactos sellados:
40
41| perfil | nodos hoy | sólo-runtime | GiB hoy | GiB sólo-runtime | grasa |
42|---|---|---|---|---|---|
43| `base` | 85 | 48 | **1,35** | 0,71 | **47 %** |
44| `cli` | 152 | 103 | **2,57** | 1,56 | 39 % |
45| `servidor` | 185 | 132 | **4,56** | 2,09 | **54 %** |
46| `escritorio-kde` | 444 | 163 | **13,05** | 5,76 | **56 %** |
47| `escritorio-gnome` | 344 | 158 | 9,63 | 3,86 | 60 % |
48| `escritorio-cosmic` | 295 | 157 | 8,48 | 4,34 | 49 % |
49| `escritorio-sway` | 281 | 143 | 7,90 | 4,11 | 48 % |
50| `escritorio-mirada` | 41 | 17 | 0,79 | 0,20 | 75 % |
51
52Y los caminos, que es lo que lo hace concreto:
53
54| en la imagen | pesa | entra por | qué es |
55|---|---|---|---|
56| `llvm21` | **1.527 MiB** | `servidor` → `rust` → `llvm21` | libs estáticas + headers de LLVM |
57| `python3` | 209 MiB | **`base` → `iputils` → `python3`** | `ping` se construye con meson |
58| `go` | 222 MiB | `cli` → `fzf` → `go` | el toolchain de Go |
59| `binutils` | 127 MiB | `base` → `bwrap` → `binutils` | ensamblador y linker |
60| `perl` | 84 MiB | `base` → `git` → `perl` | los `configure` de OpenSSL/curl |
61| `cmake` | 94 MiB | `servidor` → `rust` → `cmake` | generador de builds |
62| `nodejs` | 86 MiB | `escritorio-kde` → `atuq` → `firefox` → `nodejs` | el JS del build de Gecko |
63
64`llvm21` es el caso de libro y se verificó **en el binario, no en la receta**:
65
66```
67readelf -d /store/…-rust/usr/bin/rustc   ⇒ NEEDED librustc_driver-*.so, libc.so   (NO libLLVM)
68readelf -d /store/…-lld21/usr/bin/lld    ⇒ NEEDED libz.so.1, libgcc_s.so.1, libc.so
69recipes/llvm21.toml:75-76                ⇒ -DLLVM_BUILD_LLVM_DYLIB=OFF -DLLVM_LINK_LLVM_DYLIB=OFF
70```
71
72O sea: 1,5 G de LLVM viajan en la imagen del servidor para que **nadie** los abra. Y `servidor` es
73el perfil donde compilar **sí** es deliberado (`targets.toml:1725-1745`, «la caja que sirve el repo
74y construye, sí»): incluso ahí, el compilador es `rust` + `lld21`, no `llvm21`.
75
76### Por qué es barato: **CERO reconstrucciones**
77
78Esto no toca ninguna receta ⇒ no toca ningún `ArtifactHash` ⇒ no invalida nada. Se cambia **cómo se
79proyecta**, no qué se construye. Es lo que lo pone primero: todo lo demás de este documento cuesta
80builds.
81
82### Por qué es seguro: la ambigüedad es ENUMERABLE
83
84El miedo legítimo es la cicatriz de `gcc-libs`: si una lib dinámica se cae de la imagen, el binario
85sella, reproduce y no arranca. Medido sobre el cierre de `servidor`:
86
87> **de 185 nodos, sólo 28 contienen algún `.so`.** 144 no traen ni uno (todo el corpus es
88> `link = static`), y 13 no están en el store de este hub.
89
90Los 28: `alsa-lib binutils bzip2-shared cmus dbus elfutils-libdw ffmpeg gcc-libs gmp go
91libffi-shared libnl libudev-zero llvm21 mold mpc mpfr musl-fts musl-shared ncurses-shared perl
92python3 readline-shared sqlite-shared util-linux xz-shared zlib-shared zstd`.
93
94Entonces la regla correcta **no** es «usá sólo las aristas de runtime» (eso es lo que rompió
95firefox). Es:
96
97```
98cierre de imagen = raíces ∪ deps de runtime declaradas ∪ proveedores de los SONAME OBSERVADOS
99```
100
101y el instrumento para la tercera parte **ya existe y ya se corre**: `scripts/vigia-sonames.py`
102contesta «¿qué `.so` pide esta imagen que ningún artefacto de su cierre provee?». Hoy es un guardián
103*a posteriori*; acá pasa a ser la **definición**.
104
105### Dónde se toca, y el criterio de terminado
106
107- `scripts/yupana.py` — una tercera vista además de `cargar_colas()` y `cargar_colas(solo_build=True)`:
108  la de **imagen**. Las otras dos no se tocan: `radio` necesita la de build (re-hasheo) y el grafo de
109  estado necesita la unión (cota de «qué hay que construir»).
110- `scripts/hydrate-profile.py` — hoy «proyecta al FHS el cierre ENTERO».
111- `scripts/repo-perfil.sh` y `scripts/build-state.py` — consumen la misma membresía.
112- ⚠ **Hay DOS linajes de imagen y este es el que no está curado.** El otro —`takana bootstrap
113  product`— se arma de tres listas a mano (`STAGE1_COMPONENTS` 4, `USERLAND_COMPONENTS` 5,
114  `SERVICE_COMPONENTS` 2) y por eso `product-image.sh` puede anunciarse «lean, SIN toolchain». La
115  imagen del servidor es **producto + hidratación del perfil encima** (SDD 28 §«Receta del
116  ensamblado»), y la segunda mitad es la que trae los 2,46 GiB. Unificar los linajes es su propia
117  unidad de trabajo; **no hace falta para cobrar esto.**
118
119**Terminado cuando:** `hydrate-profile.py servidor` no proyecta `llvm21 python3 perl binutils cmake
120go`, `vigia-sonames.py` da **cero** huecos en los 8 perfiles, y `product-boot-test.sh` arranca. La
121cifra a batir: **4,56 → 2,09 GiB** en `servidor` y **13,05 → 5,76** en KDE.
122
123⚠ **Un detalle que sale de paso y no es de este frente:** el **kernel no está en ningún perfil**
124(`linux*` → `perfiles: []`); lo pasa un `KERNEL=` de script. Lo que sí viaja en los 7 es
125`linux-headers`. La imagen declara los headers del kernel y no el kernel.
126
127---
128
129## Los tres grupos, porque mezclarlos es lo que cuesta el trabajo
130
131Igual que los tres frentes de busybox, acá la diferencia que importa es **cuántas reconstrucciones
132paga cada cosa** — `[deps] build` entra en `hash_inputs`, `[deps] runtime` no:
133
134| grupo | qué es | rebuilds |
135|---|---|---|
136| **1 · proyección** | cómo se arma la imagen; ninguna receta cambia | **0** |
137| **2 · hojas** | recetas con **0 consumidores**: nadie las declara | **0** (sólo ellas) |
138| **3 · campañas** | tocar una herramienta de build que N recetas declaran | **N** |
139
140---
141
142## Grupo 2 · Las hojas: cambio moderno, coste cero
143
144Todas verificadas con `yupana`: **0 consumidores**, o sea que tocarlas no invalida a nadie.
145
146### 2.1 · `sudo` **y** `doas` en los 7 perfiles — y ninguno de los dos puede funcionar
147
148Dos herramientas para lo mismo, declaradas juntas en `base` (`targets.toml:58`) y heredadas por los
149otros 6 perfiles. `sudo` = 12,1 MiB, `doas` = 0,1 MiB. Pero el hallazgo es otro:
150
151```
152/store/…-sudo/usr/bin/sudo   → -r-xr-xr-x   (sin setuid)
153/store/…-doas/usr/bin/doas   → -r-xr-xr-x   (sin setuid)
154grep -rn "S_ISUID\|4755" crates/   → 0 hits que pongan el bit
155```
156
157Y las recetas dicen tres cosas distintas:
158
159| receta | qué afirma | ¿cierto? |
160|---|---|---|
161| `recipes/sudo.toml:57` | «el paquete lo re-aplica al hidratar» | **no** |
162| `recipes/doas.toml:38` | «el binario setuid es cosa de la hidratación» | **no** |
163| `recipes/shadow.toml:15` | «el modelo de artefactos no puede ni expresar ni garantizar un setuid» | **sí** |
164
165Es la familia de [[needed-colgante-libstdcxx]] y [[subcomando-sin-driver]]: sella, reproduce y no
166corre. **Hoy es LATENTE**, y por eso nadie lo vio: el instalador sólo pone contraseña de root
167(`takana-live-install.sh`) y los `[[user]]` de las 7 recetas que los declaran son cuentas de servicio, que no
168escalan. Se vuelve real el día que la imagen tenga un usuario humano no-root — y entonces `sudo`,
169`doas`, `su` y `passwd` fallan los cuatro.
170
171**Orden correcto, y no es «poner sudo-rs»:**
1721. **Decidir quién pone el bit** (el modelo de artefactos, la hidratación o el `.swm`). Es la
173   conversación que `shadow.toml:18` ya pidió y que sigue abierta.
1742. **Elegir UNA.** Si la respuesta es «la chica», `doas` ya ganó. Si es «la compatible con los
175   runbooks», el reemplazo moderno es **`sudo-rs`** (Rust, auditado, drop-in de `sudo`) y de paso
176   saca un setuid en C del producto.
1773. Corregir las dos recetas que afirman algo falso, gane quien gane.
178
179### 2.2 · `chrony` → `ntpd-rs`
180
181La hora es vital y el modo de fallo es traicionero: sin NTP, TLS y las firmas fallan con errores que
182dicen «firma inválida» y no «la hora» — está escrito en el propio `targets.toml`. `chrony` es C, 0
183consumidores, sólo en `servidor`. `ntpd-rs` es el reemplazo Rust hecho exactamente para este nicho
184(cliente + servidor, NTS). Receta Cargo con commit pineado, el patrón de `uutils`.
185
186⚠ Verificar antes de prometer: que cubra el modo *cliente* con el mismo contrato de arranque, y que
187su `[[service]]` encaje en arje (SDD 30). El criterio es `arjectl status` + reloj convergido en
188QEMU, no que compile.
189
190### 2.3 · `gnupg` — mirar si hay que reemplazarlo o **borrarlo**
191
192`gnupg` es raíz de `base` (`targets.toml:60`) ⇒ viaja en los 7 perfiles, 39,1 MiB, **0 consumidores**, y en todo el repo lo invoca **un
193solo script de prueba** (`scripts/product-base-system-boot-test.sh`). La firma de takana **no lo
194usa**: es ed25519 propio (`takana keygen release`, `trust/`). Entonces son dos preguntas distintas:
195
196- Como **herramienta del usuario**: el reemplazo moderno es `sequoia` (`sq`, Rust). ⚠ No confundir
197  con `recipes/sq.toml`, que es otro `sq` (neilotoole, Go, consultas a datos).
198- Como **infraestructura**: no lo es. Si nadie lo pide, es la clase «105 BORRAR» del censo de
199  busybox, no la clase «reemplazar».
200
201⚠ `libgcrypt` (18 consumidores) y `libgpg-error` (20) **se quedan** con o sin gnupg.
202
203---
204
205## Grupo 3 · Las campañas: buen cambio, factura grande
206
207### 3.1 · `meson` → `muon`: matar el intérprete de Python del sandbox
208
209| dato | valor |
210|---|---|
211| recetas que declaran `meson` | **107** |
212| de esas, que declaran también `python3` | **107 (todas)** |
213| consumidores de `python3` en total | 302 |
214| `python3` en el store | **209,0 MiB** |
215| `meson` | 3,9 MiB |
216
217El precedente ya está sentado y es del propio repo: **`samurai`** (C, 0,6 MiB, 256 consumidores)
218reemplazó a ninja. `muon` es su hermano exacto: implementación en C de `meson.build`, sin Python,
219pensada para correr con samurai. Es el cambio que convierte «`ping` arrastra 209 MiB de intérprete»
220en un no-problema del lado del build, además del lado de la imagen.
221
222⚠ **Cuesta 107 recetas re-selladas** (y sus consumidores): `deps.build` está en `hash_inputs`.
223`yupana radio meson` antes de tocar nada. Conviene **sólo pegado a otra invalidación grande**, no
224como cambio suelto.
225⚠ Y se mide primero, no se adopta: muon **no cubre el 100 %** de meson. El criterio de decisión es
226cuántas de las 107 configuran con muon sin tocar el `meson.build`; con una muestra de 10 ya se sabe
227si el frente vale.
228
229### 3.2 · `zlib` → `zlib-rs`: la lib C más consumida del corpus
230
231`zlib` tiene **122 consumidores** (+83 de `zlib-shared`) — más que ninguna otra librería del
232catálogo, y es un clásico de CVEs de memoria. `zlib-rs` publica la **misma ABI C** (`libz.a` /
233`libz.so.1`), así que en teoría es un swap sin migrar llamadores, que es justo lo que no pasa con
234`ripgrep`/`sd`/`fd`.
235
236⚠ 122 recetas re-selladas: es **la campaña más cara de este documento**. Y ⚠ hay que verificar su
237estado upstream antes de prometerlo — la misma cautela que el plan de busybox aplicó a
238`uutils/util-linux`.
239
240**Cómo se decide sin pagar la campaña:** receta `zlib-rs` publicando el mismo SONAME, swap en **un**
241consumidor dinámico (`lld21` pide `libz.so.1`), y comparar. Si ahí anda, la campaña es una decisión
242de calendario; si no, costó una receta.
243
244### 3.3 · Lo que NO es campaña aunque se parezca
245
246`openssl`: 55 consumidores, 44,1 MiB. **9 son recetas Rust** (`cargo-generate`, `cargo-outdated`,
247`cargo-udeps`, `cargo-cache`, `cosmic-settings-daemon`, `hurl`, `monolith`, `nu`, `rink`) que podrían
248usar `rustls` por *feature*. Pero los otros 34 son C y openssl seguiría viajando igual ⇒ **el
249esfuerzo no compra bytes ni superficie**. Es trabajo del día que openssl se caiga sola.
250
251---
252
253## Correcciones al plan de busybox que salen de esta medición
254
255`plan-botar-busybox.md` cifró **23 applets de trabajo real**. Dos de las ocho piezas ya están
256hechas y una **no hace falta**:
257
258| pieza | applets | estado hoy |
259|---|---|---|
260| `psmisc` | `killall killall5 pstree fuser` | ✅ `sealed`, en 7 perfiles (commit `4357b29c`) |
261| `bc` | `bc dc` | ✅ `sealed`, en 7 perfiles (commit `4357b29c`) |
262| `awk` | `awk` | ✅ resuelto **escribiendo**: `akllay`, Rust, 3.531 líneas (`86915f26`) |
263| **`kmod`** | `insmod rmmod lsmod modprobe depmod modinfo` | **no hace falta: no hay módulos** |
264
265El detalle de `kmod`, que es el que cambia la cuenta: **los cuatro kernels que arman imágenes llevan
266`-d MODULES`** — `linux-generic.toml:69`, `linux-metal`, `linux-metal-dual`, `linux-metal-kexec`. El
267único que conserva los módulos del `defconfig` es `recipes/linux.toml`, y ése **no arma ninguna
268imagen** (el SDD 28 ya lo descartó: apaga SCSI y no ve el disco de hcloud). ADR 0018 lo dice del
269otro lado: «un único bzImage, sin módulos ⇒ una sola firma».
270
271⇒ **los 6 applets de `kmod` son clase BORRAR, no clase escribir.** Vuelven el día que exista un
272módulo fuera del árbol (el tester NVIDIA es el candidato), y ese día `kmod` es una receta, no seis
273applets. Con eso el trabajo restante del censo baja de 23 a **11** (`grep` 3, `sed` 1, gestor de
274`/dev` 3, `getty` 2, `nslookup` 1), y de esos `getty` **ya se sabe que es ADR** y no escritura.
275
276---
277
278## Lo único del producto que no sale de una receta: el cargador de arranque
279
280```
281recipes/grub*.toml                       ⇒ no existe
282scripts/iso-image.sh:37                  ⇒ GRUB_LIB="${GRUB_LIB:-/usr/lib/grub/i386-pc}"
283scripts/install-image.sh:49              ⇒ idem  ·  exige grub-mkimage, grub-bios-setup
284```
285
286O sea que **el GRUB que arranca una imagen de takana es el de la caja que la construyó**, y su
287versión no entra en ningún hash. Es la rama BIOS/El-Torito; la rama UEFI **ya se escapó** por
288EFI-stub (`EFI_STUB=1`, `metal-iso.sh:104`, y el ADR 0018 con vendor path propio + reconciliador de
289NVRAM, implementado y verificado en metal).
290
291La decisión es chica y está sin tomar: **o `grub` entra al corpus como receta, o la rama BIOS se
292retira** y todo el arranque queda EFI-stub, que es además la arquitectura moderna (UKI: un PE
293firmable, sin gestor intermedio). Lo que no conviene es lo de hoy, que es depender de un binario
294ajeno sin decirlo. ⚠ Y el ADR 0018 deja abierta la de Secure Boot, que es la que lo ata.
295
296---
297
298## Arquitectura: los tres pendientes, por lo que cuestan HOY
299
3001. **ADR 0012 · el árbol de fuentes compartido — PENDIENTE desde 2026-07-22, y es el que más paga.**
301   Es lo que obliga a `JOBS=1` y a envolver todo en `flock`: la granja compila **de a uno** en una
302   caja de 6 cores. Medido en su día: al invalidarse `libdrm`, ~93 de 205 recetas KDE murieron con el
303   wrapper de zig barrido por el fetch de otra. Resolverlo (caché inmutable direccionada por
304   contenido + copia privada por build, con reflink/overlay) es **la única de estas tres que
305   multiplica la velocidad del frente entero**. Todo lo demás de este documento se construye más
306   rápido después de esto.
3072. **Las 28 recetas `*-shared`.** `zlib`/`zlib-shared`, `pcre2`/`pcre2-shared`, `glib`/`glib-shared`…
308   168,4 MiB de artefactos y, sobre todo, **28 builds y 28 puntos de mantenimiento duplicados** para
309   emitir dos formas del mismo código. La arquitectura correcta es una receta con **varias salidas**
310   (estática / dinámica / headers), que además es lo que haría al vigía de sonames innecesario en su
311   caso más común. ⚠ Re-hashea; va después del §1, que es el que hace que la distinción importe.
3123. **Los dos linajes de imagen** (tres listas a mano vs. clausura del perfil). Hoy conviven y
313   divergen en silencio: es la misma forma que los `hydrate-*.sh` contra `targets.toml`
314   ([[hidratar-desde-el-perfil-no-el-script]]) y que el `sshd` que entra por
315   `product-userland-from-repo.sh` y no por el manifiesto — lo dice el propio `targets.toml`.
316
317---
318
319## Lo que NO conviene tocar, y por qué (para que no se proponga en tres semanas)
320
321| candidato | por qué no |
322|---|---|
323| `openssh` | no hay reemplazo Rust drop-in de un servidor SSH. Es el acceso a la caja: un experimento acá se paga con una máquina inalcanzable |
324| `linux-pam` | 5 consumidores, todos greeters de escritorio. No hay reemplazo; la deuda está contenida y nombrada |
325| `cups` / `bluez` | 291,8 y 35,5 MiB en los 4 escritorios, **ya decidido a conciencia** (`targets.toml`: viajan como paquete y NO se arrancan como servicio). Es el punto 8 de PUBLICABLE, no un olvido |
326| `NetworkManager` | 50,3 MiB, pero los applets de los 4 escritorios le hablan **por D-Bus**. Es migración de llamadores, no borrado — la lección de `ifconfig` del censo de busybox |
327| el demonio de `dbus` | 124 consumidores son **clientes** de libdbus; cambiar el demonio (busd/dbus-broker) es posible y no urge |
328| forkear `ripgrep`/`sd`/`fd` | ya cerrado en `plan-botar-busybox.md`: se usan como **librerías**, no se forkean |
329| deduplicar `work/sources` | el árbol por receta ES el aislamiento del ADR 0012. Deduplicar *de paso* re-crea la carrera |
330
331---
332
333## Orden de ataque
334
335| # | unidad | rebuilds | gana |
336|---|---|---|---|
337| 1 | cierre de imagen `build` vs `runtime` (+ el vigía como definición) | **0** | −2,5 GiB en servidor, −7,3 en KDE, y el toolchain fuera del producto |
338| 2 | decidir el setuid y quedarse con **una** de `sudo`/`doas` | 0 | cierra un «sella y no corre» antes de que sea visible |
339| 3 | `kmod` a BORRAR en el censo de busybox (23 → 11 applets) | 0 | no se escribe lo que nadie necesita |
340| 4 | la rama BIOS: receta de `grub` o retirarla | 0 | ningún binario ajeno sin hash en el arranque |
341| 5 | `chrony` → `ntpd-rs` | 0 | una hoja C vital menos, con drop-in Rust |
342| 6 | **ADR 0012** | — | la granja deja de compilar de a uno |
343| 7 | `meson` → `muon` (medir 10 antes) | 107 | un intérprete menos en el sandbox |
344| 8 | `zlib-rs` en UN consumidor, para decidir | 1 | la lib C más consumida del corpus, con la misma ABI |
345
346Los cinco primeros **no cuestan una sola reconstrucción**. Eso es lo que los pone antes: la lección
347de `uutils`/`brush`/`foot` —trabajo hecho y no cobrado— aplicada al calendario.

plan-que-sigue-tras-busybox.md

Plan — con qué conviene hacer lo de busybox, MEDIDO

Fecha: 2026-09-22 · Pregunta: «botado busybox, ¿con qué conviene hacer lo mismo? ¿Hay que rusterizar o soberanizar algo vital, hay opciones modernas a cosas tradicionales, o hay una arquitectura mejor?»

Instrumento (todo reproducible, nada escrito a mano):

# grafo y clases: los 950 nodos con su clase, hash, perfiles y dependientes
python3 -c 'import json;json.load(open("docs/state/build-state.json"))'
# aristas y membresía: la MISMA función que usan el grafo, la hidratación y el repo firmado
python3 -c 'import sys;sys.path.insert(0,"scripts");import yupana;yupana.cargar_colas()'
# bytes: tamaño real de cada artefacto sellado
cd /store && du -sk --apparent-size *          # 1.435 artefactos, 59,8 GiB
# runtime real: quién pide qué SONAME
readelf -d <artefacto>/usr/bin/<bin>

El hallazgo que ordena todo, y no es una pieza de userland

La clausura que define una imagen es build ∪ runtime. Entonces cada imagen de takana lleva adentro la cadena de compilación: llvm21, go, python3, binutils, cmake, perl, nasm, bison, flex, m4, meson, wasi-sdk, nodejs.

Es exactamente la forma del caso busybox, y por eso vale la pena verlo dos veces: busybox nunca estuvo declarado en targets.toml —llegaba a los 7 perfiles por la clausura, porque 24 recetas lo pedían en [deps] build—, y sacarlo no costó escribir una línea de userland (paso 6 de plan-botar-busybox.md). Lo que ese paso arregló para un nombre, esto lo arregla para la clase entera.

La definición está en scripts/yupana.py:98 (_deps) y en scripts/build-state.py:128, y en las dos hay un comentario que explica por qué es la unión: firefox declaraba runtime = ["gcc-libs"], nadie leía ese campo, y la imagen se armaba sin libstdc++.so.6. El arreglo fue correcto y la unión es lo que hay que afinar, no lo que hay que deshacer — ver el riesgo más abajo.

Lo que cuesta, en nodos y en bytes

Clausura de hoy contra «raíces + aristas de runtime», medido sobre los artefactos sellados:

perfilnodos hoysólo-runtimeGiB hoyGiB sólo-runtimegrasa
base85481,350,7147 %
cli1521032,571,5639 %
servidor1851324,562,0954 %
escritorio-kde44416313,055,7656 %
escritorio-gnome3441589,633,8660 %
escritorio-cosmic2951578,484,3449 %
escritorio-sway2811437,904,1148 %
escritorio-mirada41170,790,2075 %

Y los caminos, que es lo que lo hace concreto:

en la imagenpesaentra porqué es
llvm211.527 MiBservidor → rust → llvm21libs estáticas + headers de LLVM
python3209 MiBbase → iputils → python3ping se construye con meson
go222 MiBcli → fzf → goel toolchain de Go
binutils127 MiBbase → bwrap → binutilsensamblador y linker
perl84 MiBbase → git → perllos configure de OpenSSL/curl
cmake94 MiBservidor → rust → cmakegenerador de builds
nodejs86 MiBescritorio-kde → atuq → firefox → nodejsel JS del build de Gecko

llvm21 es el caso de libro y se verificó en el binario, no en la receta:

readelf -d /store/…-rust/usr/bin/rustc   ⇒ NEEDED librustc_driver-*.so, libc.so   (NO libLLVM)
readelf -d /store/…-lld21/usr/bin/lld    ⇒ NEEDED libz.so.1, libgcc_s.so.1, libc.so
recipes/llvm21.toml:75-76                ⇒ -DLLVM_BUILD_LLVM_DYLIB=OFF -DLLVM_LINK_LLVM_DYLIB=OFF

O sea: 1,5 G de LLVM viajan en la imagen del servidor para que nadie los abra. Y servidor es el perfil donde compilar sí es deliberado (targets.toml:1725-1745, «la caja que sirve el repo y construye, sí»): incluso ahí, el compilador es rust + lld21, no llvm21.

Por qué es barato: CERO reconstrucciones

Esto no toca ninguna receta ⇒ no toca ningún ArtifactHash ⇒ no invalida nada. Se cambia cómo se proyecta, no qué se construye. Es lo que lo pone primero: todo lo demás de este documento cuesta builds.

Por qué es seguro: la ambigüedad es ENUMERABLE

El miedo legítimo es la cicatriz de gcc-libs: si una lib dinámica se cae de la imagen, el binario sella, reproduce y no arranca. Medido sobre el cierre de servidor:

de 185 nodos, sólo 28 contienen algún .so. 144 no traen ni uno (todo el corpus es link = static), y 13 no están en el store de este hub.

Los 28: alsa-lib binutils bzip2-shared cmus dbus elfutils-libdw ffmpeg gcc-libs gmp go libffi-shared libnl libudev-zero llvm21 mold mpc mpfr musl-fts musl-shared ncurses-shared perl python3 readline-shared sqlite-shared util-linux xz-shared zlib-shared zstd.

Entonces la regla correcta no es «usá sólo las aristas de runtime» (eso es lo que rompió firefox). Es:

cierre de imagen = raíces ∪ deps de runtime declaradas ∪ proveedores de los SONAME OBSERVADOS

y el instrumento para la tercera parte ya existe y ya se corre: scripts/vigia-sonames.py contesta «¿qué .so pide esta imagen que ningún artefacto de su cierre provee?». Hoy es un guardián a posteriori; acá pasa a ser la definición.

Dónde se toca, y el criterio de terminado

Terminado cuando: hydrate-profile.py servidor no proyecta llvm21 python3 perl binutils cmake go, vigia-sonames.py da cero huecos en los 8 perfiles, y product-boot-test.sh arranca. La cifra a batir: 4,56 → 2,09 GiB en servidor y 13,05 → 5,76 en KDE.

⚠ Un detalle que sale de paso y no es de este frente: el kernel no está en ningún perfil (linux* → perfiles: []); lo pasa un KERNEL= de script. Lo que sí viaja en los 7 es linux-headers. La imagen declara los headers del kernel y no el kernel.


Los tres grupos, porque mezclarlos es lo que cuesta el trabajo

Igual que los tres frentes de busybox, acá la diferencia que importa es cuántas reconstrucciones paga cada cosa — [deps] build entra en hash_inputs, [deps] runtime no:

grupoqué esrebuilds
1 · proyeccióncómo se arma la imagen; ninguna receta cambia0
2 · hojasrecetas con 0 consumidores: nadie las declara0 (sólo ellas)
3 · campañastocar una herramienta de build que N recetas declaranN

Grupo 2 · Las hojas: cambio moderno, coste cero

Todas verificadas con yupana: 0 consumidores, o sea que tocarlas no invalida a nadie.

2.1 · sudo y doas en los 7 perfiles — y ninguno de los dos puede funcionar

Dos herramientas para lo mismo, declaradas juntas en base (targets.toml:58) y heredadas por los otros 6 perfiles. sudo = 12,1 MiB, doas = 0,1 MiB. Pero el hallazgo es otro:

/store/…-sudo/usr/bin/sudo   → -r-xr-xr-x   (sin setuid)
/store/…-doas/usr/bin/doas   → -r-xr-xr-x   (sin setuid)
grep -rn "S_ISUID\|4755" crates/   → 0 hits que pongan el bit

Y las recetas dicen tres cosas distintas:

recetaqué afirma¿cierto?
recipes/sudo.toml:57«el paquete lo re-aplica al hidratar»no
recipes/doas.toml:38«el binario setuid es cosa de la hidratación»no
recipes/shadow.toml:15«el modelo de artefactos no puede ni expresar ni garantizar un setuid»sí

Es la familia de [[needed-colgante-libstdcxx]] y [[subcomando-sin-driver]]: sella, reproduce y no corre. Hoy es LATENTE, y por eso nadie lo vio: el instalador sólo pone contraseña de root (takana-live-install.sh) y los [[user]] de las 7 recetas que los declaran son cuentas de servicio, que no escalan. Se vuelve real el día que la imagen tenga un usuario humano no-root — y entonces sudo, doas, su y passwd fallan los cuatro.

Orden correcto, y no es «poner sudo-rs»:

  1. Decidir quién pone el bit (el modelo de artefactos, la hidratación o el .swm). Es la conversación que shadow.toml:18 ya pidió y que sigue abierta.
  2. Elegir UNA. Si la respuesta es «la chica», doas ya ganó. Si es «la compatible con los runbooks», el reemplazo moderno es sudo-rs (Rust, auditado, drop-in de sudo) y de paso saca un setuid en C del producto.
  3. Corregir las dos recetas que afirman algo falso, gane quien gane.

2.2 · chrony → ntpd-rs

La hora es vital y el modo de fallo es traicionero: sin NTP, TLS y las firmas fallan con errores que dicen «firma inválida» y no «la hora» — está escrito en el propio targets.toml. chrony es C, 0 consumidores, sólo en servidor. ntpd-rs es el reemplazo Rust hecho exactamente para este nicho (cliente + servidor, NTS). Receta Cargo con commit pineado, el patrón de uutils.

⚠ Verificar antes de prometer: que cubra el modo cliente con el mismo contrato de arranque, y que su [[service]] encaje en arje (SDD 30). El criterio es arjectl status + reloj convergido en QEMU, no que compile.

2.3 · gnupg — mirar si hay que reemplazarlo o borrarlo

gnupg es raíz de base (targets.toml:60) ⇒ viaja en los 7 perfiles, 39,1 MiB, 0 consumidores, y en todo el repo lo invoca un solo script de prueba (scripts/product-base-system-boot-test.sh). La firma de takana no lo usa: es ed25519 propio (takana keygen release, trust/). Entonces son dos preguntas distintas:

⚠ libgcrypt (18 consumidores) y libgpg-error (20) se quedan con o sin gnupg.


Grupo 3 · Las campañas: buen cambio, factura grande

3.1 · meson → muon: matar el intérprete de Python del sandbox

datovalor
recetas que declaran meson107
de esas, que declaran también python3107 (todas)
consumidores de python3 en total302
python3 en el store209,0 MiB
meson3,9 MiB

El precedente ya está sentado y es del propio repo: samurai (C, 0,6 MiB, 256 consumidores) reemplazó a ninja. muon es su hermano exacto: implementación en C de meson.build, sin Python, pensada para correr con samurai. Es el cambio que convierte «ping arrastra 209 MiB de intérprete» en un no-problema del lado del build, además del lado de la imagen.

⚠ Cuesta 107 recetas re-selladas (y sus consumidores): deps.build está en hash_inputs. yupana radio meson antes de tocar nada. Conviene sólo pegado a otra invalidación grande, no como cambio suelto. ⚠ Y se mide primero, no se adopta: muon no cubre el 100 % de meson. El criterio de decisión es cuántas de las 107 configuran con muon sin tocar el meson.build; con una muestra de 10 ya se sabe si el frente vale.

3.2 · zlib → zlib-rs: la lib C más consumida del corpus

zlib tiene 122 consumidores (+83 de zlib-shared) — más que ninguna otra librería del catálogo, y es un clásico de CVEs de memoria. zlib-rs publica la misma ABI C (libz.a / libz.so.1), así que en teoría es un swap sin migrar llamadores, que es justo lo que no pasa con ripgrep/sd/fd.

⚠ 122 recetas re-selladas: es la campaña más cara de este documento. Y ⚠ hay que verificar su estado upstream antes de prometerlo — la misma cautela que el plan de busybox aplicó a uutils/util-linux.

Cómo se decide sin pagar la campaña: receta zlib-rs publicando el mismo SONAME, swap en un consumidor dinámico (lld21 pide libz.so.1), y comparar. Si ahí anda, la campaña es una decisión de calendario; si no, costó una receta.

3.3 · Lo que NO es campaña aunque se parezca

openssl: 55 consumidores, 44,1 MiB. 9 son recetas Rust (cargo-generate, cargo-outdated, cargo-udeps, cargo-cache, cosmic-settings-daemon, hurl, monolith, nu, rink) que podrían usar rustls por feature. Pero los otros 34 son C y openssl seguiría viajando igual ⇒ el esfuerzo no compra bytes ni superficie. Es trabajo del día que openssl se caiga sola.


Correcciones al plan de busybox que salen de esta medición

plan-botar-busybox.md cifró 23 applets de trabajo real. Dos de las ocho piezas ya están hechas y una no hace falta:

piezaappletsestado hoy
psmisckillall killall5 pstree fuser✅ sealed, en 7 perfiles (commit 4357b29c)
bcbc dc✅ sealed, en 7 perfiles (commit 4357b29c)
awkawk✅ resuelto escribiendo: akllay, Rust, 3.531 líneas (86915f26)
kmodinsmod rmmod lsmod modprobe depmod modinfono hace falta: no hay módulos

El detalle de kmod, que es el que cambia la cuenta: los cuatro kernels que arman imágenes llevan -d MODULES — linux-generic.toml:69, linux-metal, linux-metal-dual, linux-metal-kexec. El único que conserva los módulos del defconfig es recipes/linux.toml, y ése no arma ninguna imagen (el SDD 28 ya lo descartó: apaga SCSI y no ve el disco de hcloud). ADR 0018 lo dice del otro lado: «un único bzImage, sin módulos ⇒ una sola firma».

⇒ los 6 applets de kmod son clase BORRAR, no clase escribir. Vuelven el día que exista un módulo fuera del árbol (el tester NVIDIA es el candidato), y ese día kmod es una receta, no seis applets. Con eso el trabajo restante del censo baja de 23 a 11 (grep 3, sed 1, gestor de /dev 3, getty 2, nslookup 1), y de esos getty ya se sabe que es ADR y no escritura.


Lo único del producto que no sale de una receta: el cargador de arranque

recipes/grub*.toml                       ⇒ no existe
scripts/iso-image.sh:37                  ⇒ GRUB_LIB="${GRUB_LIB:-/usr/lib/grub/i386-pc}"
scripts/install-image.sh:49              ⇒ idem  ·  exige grub-mkimage, grub-bios-setup

O sea que el GRUB que arranca una imagen de takana es el de la caja que la construyó, y su versión no entra en ningún hash. Es la rama BIOS/El-Torito; la rama UEFI ya se escapó por EFI-stub (EFI_STUB=1, metal-iso.sh:104, y el ADR 0018 con vendor path propio + reconciliador de NVRAM, implementado y verificado en metal).

La decisión es chica y está sin tomar: o grub entra al corpus como receta, o la rama BIOS se retira y todo el arranque queda EFI-stub, que es además la arquitectura moderna (UKI: un PE firmable, sin gestor intermedio). Lo que no conviene es lo de hoy, que es depender de un binario ajeno sin decirlo. ⚠ Y el ADR 0018 deja abierta la de Secure Boot, que es la que lo ata.


Arquitectura: los tres pendientes, por lo que cuestan HOY

  1. ADR 0012 · el árbol de fuentes compartido — PENDIENTE desde 2026-07-22, y es el que más paga. Es lo que obliga a JOBS=1 y a envolver todo en flock: la granja compila de a uno en una caja de 6 cores. Medido en su día: al invalidarse libdrm, ~93 de 205 recetas KDE murieron con el wrapper de zig barrido por el fetch de otra. Resolverlo (caché inmutable direccionada por contenido + copia privada por build, con reflink/overlay) es la única de estas tres que multiplica la velocidad del frente entero. Todo lo demás de este documento se construye más rápido después de esto.
  2. Las 28 recetas *-shared. zlib/zlib-shared, pcre2/pcre2-shared, glib/glib-shared… 168,4 MiB de artefactos y, sobre todo, 28 builds y 28 puntos de mantenimiento duplicados para emitir dos formas del mismo código. La arquitectura correcta es una receta con varias salidas (estática / dinámica / headers), que además es lo que haría al vigía de sonames innecesario en su caso más común. ⚠ Re-hashea; va después del §1, que es el que hace que la distinción importe.
  3. Los dos linajes de imagen (tres listas a mano vs. clausura del perfil). Hoy conviven y divergen en silencio: es la misma forma que los hydrate-*.sh contra targets.toml ([[hidratar-desde-el-perfil-no-el-script]]) y que el sshd que entra por product-userland-from-repo.sh y no por el manifiesto — lo dice el propio targets.toml.

Lo que NO conviene tocar, y por qué (para que no se proponga en tres semanas)

candidatopor qué no
opensshno hay reemplazo Rust drop-in de un servidor SSH. Es el acceso a la caja: un experimento acá se paga con una máquina inalcanzable
linux-pam5 consumidores, todos greeters de escritorio. No hay reemplazo; la deuda está contenida y nombrada
cups / bluez291,8 y 35,5 MiB en los 4 escritorios, ya decidido a conciencia (targets.toml: viajan como paquete y NO se arrancan como servicio). Es el punto 8 de PUBLICABLE, no un olvido
NetworkManager50,3 MiB, pero los applets de los 4 escritorios le hablan por D-Bus. Es migración de llamadores, no borrado — la lección de ifconfig del censo de busybox
el demonio de dbus124 consumidores son clientes de libdbus; cambiar el demonio (busd/dbus-broker) es posible y no urge
forkear ripgrep/sd/fdya cerrado en plan-botar-busybox.md: se usan como librerías, no se forkean
deduplicar work/sourcesel árbol por receta ES el aislamiento del ADR 0012. Deduplicar de paso re-crea la carrera

Orden de ataque

#unidadrebuildsgana
1cierre de imagen build vs runtime (+ el vigía como definición)0−2,5 GiB en servidor, −7,3 en KDE, y el toolchain fuera del producto
2decidir el setuid y quedarse con una de sudo/doas0cierra un «sella y no corre» antes de que sea visible
3kmod a BORRAR en el censo de busybox (23 → 11 applets)0no se escribe lo que nadie necesita
4la rama BIOS: receta de grub o retirarla0ningún binario ajeno sin hash en el arranque
5chrony → ntpd-rs0una hoja C vital menos, con drop-in Rust
6ADR 0012—la granja deja de compilar de a uno
7meson → muon (medir 10 antes)107un intérprete menos en el sandbox
8zlib-rs en UN consumidor, para decidir1la lib C más consumida del corpus, con la misma ABI

Los cinco primeros no cuestan una sola reconstrucción. Eso es lo que los pone antes: la lección de uutils/brush/foot —trabajo hecho y no cobrado— aplicada al calendario.