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 1534Shell 199Rust 109Markdown 993713 Sutichaykuna

⎇ 27d2dfd7702d36cdd178f8b9294b924958b8a564 docs/plan-botar-busybox.md Markdown · 332 siq'ikuna · 20.2 KB Ñawpa willay ch'uya

brush: la causa era el desescapado de barras dentro de comillas invertidas — reportada, y adelantar el pin no sirve 27d2dfd77 · Sergio ·

1# Plan — botar busybox, MEDIDO
2
3**Fecha:** 2026-09-21 · **Instrumento:** los 401 symlinks de applet del artefacto sellado
4`2a2b1280…-busybox` (contados en el store del worker), cruzados con el estado y la membresía de
5perfil de cada receta en `docs/state/build-state.json` y con `grep '[deps]' recipes/*.toml`.
6**Tabla applet-por-applet:** `docs/state/busybox-applets.tsv` (401 filas, generada, no escrita a mano).
7
8## Por qué existe este documento
9
10La pregunta fue «¿qué hay que hacer para botar busybox?», con la sospecha de que faltaba escribir
11mucho userland en Rust. **El número dice lo contrario, y el hueco está en otro lado:**
12
13> De los 401 applets, **148 ya tienen dueño sellado Y en perfil** (el applet es redundante en la
14> imagen), **125 tienen dueño sellado que NO viaja en ninguna imagen** (trabajo hecho y no cobrado),
15> y de los **128 sin dueño, 105 son basura que se borra**. El trabajo de escritura real son
16> **23 applets** repartidos en 8 piezas.
17
18Es la lección de `foot` y del §D de `plan-barrido-perfiles-y-servicios.md` otra vez: `uutils`,
19`findutils`, `findutils-xargs`, `diffutils` y **`brush`** están todos `sealed` con `perfiles: []`.
20Se construyeron, se sellaron, y ninguna imagen los lleva.
21
22## Son TRES frentes, no uno — y esto es lo que hace que «botar busybox» se lea como imposible
23
24Mezclarlos es el error que cuesta el trabajo. Tienen dificultad, dueño y riesgo distintos:
25
26| frente | qué es busybox ahí | dificultad |
27|---|---|---|
28| **A · Sandbox de build** | `[deps] build = [… "busybox" …]` en **24 recetas** — es el `sh`+coreutils que el sandbox del lab trae (`recipes/jq.toml:5-7`, `recipes/zlib-ng.toml:11`). Más el `/toolchain` mixto de Alpine con el swap de `docs/11-bootstrap.md:234-240` | media — no es runtime, no toca el producto |
29| **B · Runtime del producto** | los applets de `/bin` y `/sbin` de las imágenes. `USERLAND_COMPONENTS` (`crates/takana-bootstrap/src/lib.rs:406`) ya los ensombrece con los binarios Rust y retira el homónimo de los otros dirs (`lib.rs:676`) | **baja — la máquina ya está construida, falta munición** |
30| **C · Stage 1 / self-host / atestación** | `STAGE1_COMPONENTS` (`lib.rs:172`, el 4/4), `ATTEST_PATHS` como raíz-de-confianza (`lib.rs:465`), y el `exec` de las cards de `getty` y `sshd` (`lib.rs:429`, `:240`) | **alta — es el ADR 0008, y toca la evidencia de reproducibilidad `of_tree=9adefb82…`** |
31
32⚠ **La membresía de perfil de busybox es del frente A, no del B.** `busybox` figura en `cli`,
33`servidor`, los cuatro escritorios y `metal-tigerlake`, pero **no está declarado como raíz en
34`docs/state/targets.toml`** (los únicos hits ahí son comentarios). Llega por la clausura, porque 24
35recetas lo declaran build-dep: `abseil-cpp chrony cronie crun dwarves firmware-tigerlake
36ia-modelo-embeddings intel-ucode jq libmnl libnftnl linux linux-firmware llama-cpp logrotate
37nftables opensmtpd popt protobuf qdrant sof-firmware valkey wireless-regdb zlib-ng`. **Sacarlo de
38las imágenes empieza por ahí y no requiere escribir ni una línea de userland.**
39
40## El reparto de los 401
41
42| estado del dueño | n | qué significa |
43|---|---|---|
44| **sellada + en perfil** | 148 | el reemplazo ya viaja en la imagen: el applet es redundante hoy |
45| **sellada, sin perfil** | 125 | construido, sellado, y no viaja: falta declararlo |
46| **sin dueño** | 128 | de los cuales **105 son `BORRAR`** y **23 son trabajo real** |
47
48### 148 · Ya cubiertos y en la imagen — el applet sobra
49
50`util-linux` 48 · `iproute2` 19 · `procps-ng` 12 · `shadow` 14 · `kbd` 12 · `xz` 6 · `iputils` 5 ·
51`e2fsprogs` 5 · `bzip2` 3 · `cronie` 3 · `dhcpcd` 3 · `ncurses` 2 · `dosfstools` 2 · `chrony` 2 ·
52`vim` 2 · `less` `lsof` `pciutils` `usbutils` `mandoc` `socat` `patch` `binutils` `tar` `wget` 1 c/u.
53
54⚠ **24 de estos 148 NO son drop-in**, y están marcados en el TSV. El caso grande es la red:
55`iproute2` da `ip`/`ss`, **no** `ifconfig`/`route`/`arp`/`netstat` — eso es net-tools, que no está en
56el corpus. Igual con `addgroup`/`adduser` (shadow da `groupadd`/`useradd`), `udhcpc` (dhcpcd es otro
57cliente con otro contrato de hooks) y `nc` (socat tiene otra sintaxis). **Cada uno de esos 24 es una
58migración de llamadores, no un borrado.** Mirar quién los invoca en `scripts/` y en las cards antes.
59
60### 125 · Sellados y sin viajar — la munición que ya está fabricada
61
62| dueño | n | qué cubre |
63|---|---|---|
64| `uutils` | 90 | todo el núcleo POSIX: `ls cp mv rm cat head tail sort cut tr wc stat date dd chmod chown …` |
65| `arje-zero` | 20 | `init linuxrc runlevel halt poweroff reboot run-init switch_root` + los 12 de runit (`runsv sv svc svlogd chpst envdir setuidgid softlimit start-stop-daemon …`) |
66| `hammerd` | 4 | `syslogd klogd logger logread` |
67| `brush` | 3 | **`sh ash hush`** |
68| `gzip` | 3 | `gzip gunzip zcat` |
69| `diffutils` | 2 | `diff cmp` |
70| `findutils` / `findutils-xargs` / `libarchive` | 3 | `find` · `xargs` · `cpio` |
71
72**Los 24 de `arje-zero` y `hammerd` no cuestan nada**: el reemplazo ya corre en el producto. Son
73applets instalados que nadie ejecuta. Salen del `defconfig` y se prueba que la imagen arranque igual.
74
75### 128 · Sin dueño
76
77**105 se borran** (no se reemplazan, salen del `defconfig`): `dpkg dpkg-deb rpm rpm2cpio httpd ftpd
78ftpget ftpput tftp tftpd telnet telnetd inetd dnsd sendmail popmaildir lpd lpq lpr ubi* nand*
79nbd-client i2c* hdparm fdformat setserial powertop bootchartd fbsplash conspy ether-wake udhcpd
80udpsvd tcpsvd watchdog whois tree which unzip …` (lista completa en el TSV).
81
82**23 son el trabajo real:**
83
84| pieza | applets | qué es |
85|---|---|---|
86| **kmod** | `insmod rmmod lsmod modprobe depmod modinfo` (6) | no hay receta kmod en el corpus. En Rust: `finit_module` + parseo de `modules.dep`. Bien acotado |
87| **psmisc** | `killall killall5 pstree fuser` (4) | **`procps-ng` NO los trae.** Traer receta psmisc (C) o escribir |
88| **grep POSIX** | `grep egrep fgrep` (3) | `grep` GNU está sellado (sin perfil) como puente. El Rust nativo se escribe sobre `grep-matcher`/`grep-regex`/`grep-searcher` |
89| **gestor de /dev** | `mdev makedevs uevent` (3) | decisión: lo absorbe arje, o entra una receta eudev/mdevd |
90| **bc** | `bc dc` (2) | traer receta |
91| **getty** | `getty cttyhack` (2) | hoy la card de getty ejecuta `/bin/busybox` (`lib.rs:429`) |
92| **awk** | `awk` (1) | decisión abierta, ver abajo |
93| **sed** | `sed` (1) | `sed` GNU sellado y en los 7 perfiles; el Rust nativo está por escribir |
94| **dns** | `nslookup` (1) | traer receta bind-tools/doggo |
95
96## Las tres clases de reemplazo — y cuándo forkear (nunca)
97
98La pregunta era si hay que forkear los que «existen pero no son exactos». **No, y hay tres clases
99distintas que conviene no mezclar:**
100
101**1 · Adopción exacta.** `uutils/coreutils`, `uutils/findutils`, `uutils/diffutils`: reimplementación
102madura, MIT, receta Cargo con commit pinneado (ADR 0006), multicall. El patrón está probado y
103documentado en la cabecera de `recipes/uutils.toml`. Lo que falta de esta clase es
104**`uutils/procps`** y **`uutils/util-linux`** — receta calcada, coste bajo. ⚠ **Verificar su estado
105upstream antes de prometer los 60 applets que cubrirían**: `util-linux` en Rust era embrionario.
106
107**2 · «Existe pero no es exacto» — y forkear es la trampa.** `ripgrep` no es `grep` POSIX, `sd` no es
108`sed`, `fd` no es `find` (por eso la receta buena es `uutils/findutils` y no `fd`), `procs` no es
109`ps`, `bottom` no es `top`, `helix` no es `vi`. Todas están selladas y en los 7 perfiles, y **ninguna
110sirve de reemplazo**: cambian las banderas. Forkear ripgrep para que hable POSIX es peor que
111escribir — heredás su arquitectura de recursión+ignore y peleás contra ella para siempre. Lo correcto
112es usarlas **como librerías**: `grep-matcher`/`grep-regex`/`grep-searcher` están publicadas
113justamente para eso, y un `grep` POSIX encima son unos cientos de líneas.
114
115**3 · `awk` es el caso feo, y hay que decidirlo, no resolverlo de paso.** `recipes/goawk.toml` existe
116y está sellada, pero **goawk es Go, no Rust** — no cumple la premisa de soberanía de cadena de la
117Etapa C. `frawk` es Rust pero es otro dialecto (JIT, semántica propia). `gawk` es C con extensiones
118GNU que los `configure` del corpus sí usan. Las tres opciones son legítimas y ninguna es gratis.
119
120## El bloqueante, y la buena noticia
121
122**Sin `sh` POSIX no se bota busybox en ningún frente**: es el `exec` de las cards, el `/bin/sh` del
123producto y el shell del sandbox. Lo esperado era que fuera un proyecto de meses.
124
125**`recipes/brush.toml` (0.4.0, reubeno/brush, MIT, Rust, compat bash) está `sealed`.** Compila
126estático musl con zig cc y produce `/usr/bin/brush`.
127
128⚠ **Y está sin validar como `sh`.** La receta es un import crudo de nixpkgs y lo dice en su primera
129línea: «PUNTO DE PARTIDA, no final». No está en ningún perfil, nadie lo ejecutó como `/bin/sh`, y el
130sandbox de build es el consumidor más duro que existe (`configure` de autotools). **La unidad de
131trabajo #1 es medirlo, no adoptarlo.**
132
133## Orden de ataque
134
135Cada paso es una unidad de trabajo con su criterio de terminado. Los tres primeros **no tocan el
136frente C** y se pueden hacer hoy.
137
1381. **Recortar el `defconfig`.** Sacar los 105 de `BORRAR` + los 24 que `arje-zero`/`hammerd` ya
139   reemplazan = **129 applets menos, cero código escrito**. Terminado cuando la imagen arranca, el
140   E2E de QEMU pasa y `find . -type l | wc -l` baja de 401 a ~272.
1412. **Declarar la munición que ya existe.** `uutils`, `findutils`, `findutils-xargs`, `diffutils`,
142   `brush`, `gzip`, `libarchive` a los perfiles de `targets.toml`. Terminado cuando `build-state.py`
143   los marca con perfil y la imagen los lleva.
1443. ~~**Medir `brush` como `/bin/sh`.**~~ **HECHO, 2026-09-21 — veredicto abajo: no sirve todavía
145   para el sandbox.** Queda pendiente medirlo como `/bin/sh` del PRODUCTO (cards + `product-boot-test.sh`),
146   que es un consumidor distinto y mucho más blando.
1474. **Las dos recetas de adopción baratas** (`uutils/procps`, `uutils/util-linux`), previa
148   verificación de madurez upstream. Cubren ~60 applets de los 148 «ya cubiertos por C».
1495. **`grep` y `sed` POSIX en Rust.** Es la escritura de mejor relación coste/beneficio y desbloquea
150   el `/toolchain` del frente A.
1516. **Migrar los 24 build-deps.** Cambiar `busybox` por el conjunto real en `[deps] build` de las 24
152   recetas. Terminado cuando `busybox` sale de los 7 perfiles.
1537. **La cola larga:** kmod, psmisc, gestor de /dev, getty, bc, dns. Después del shell, no antes.
1548. **ADR — `awk`**, y **ADR — el frente C** (sustituir busybox en `STAGE1_COMPONENTS` y
155   `ATTEST_PATHS`). No antes de que 1–7 estén cerrados.
156
157## La métrica, y el guardián que falta
158
159`docs/state/busybox-applets.tsv` convierte esto en un número que baja: **401 → 272 → … → 0**.
160Regenerarlo es contar los symlinks del artefacto sellado y cruzarlos con `build-state.json`.
161
162**Falta un guardián**, y sin él el paso 1 se deshace solo: un script que falle si alguna receta,
163script o card invoca un applet que ya salió del `defconfig`. Hoy nada lo detectaría — el fallo
164aparecería en boot, en otra máquina, semanas después.
165
166## Lo que este documento NO midió (y hay que decirlo así)
167
168- **El reparto applet→destino es juicio, no medida.** Lo medido son los 401 symlinks, el estado
169  `sealed`/perfiles de cada receta y las 24 recetas con `busybox` en `[deps] build`. La asignación de
170  dueño la escribí yo y hay que revisarla receta a receta.
171- **No se verificó la equivalencia de banderas de ninguna pareja.** Los 24 marcados `NO-drop-in` son
172  los que sé que difieren; el resto dice `sin-verificar` y eso es literal, no una forma de decir «va
173  a andar».
174- **No se midió qué applets usa el producto de verdad.** El `defconfig` instala 401 porque nadie
175  eligió; cuáles se ejecutan en una imagen viva no está medido. Eso abarataría el paso 1.
176- **`uutils/procps` y `uutils/util-linux` no se verificaron upstream.** Los ~60 applets que les
177  atribuyo son una hipótesis de trabajo.
178
179---
180
181# Veredicto del paso 3 — `brush` como `/bin/sh`, MEDIDO
182
183**Fecha:** 2026-09-21 · **Dónde:** `dev.gioser.net`, todo bajo `flock -o work/.farm-build.lock` ·
184**Contra qué:** busybox 1.36.1 `sh` (ash) como control, en la misma caja y la misma corrida.
185
186> **No sirve todavía como `/bin/sh` del SANDBOX DE BUILD.** Pasa el banco POSIX igual que busybox,
187> parsea todo el corpus, y aun así **0 de 3 recetas autotools construyen** (control: 3/3). Lo que
188> falla no es su parser: es que el `libtool` que su `configure` genera sale con un nivel de escapado
189> de menos y después no lo parsea ningún shell, ni él ni busybox.
190
191## Lo que se construyó
192
193`takana build recipes/brush.toml` en el worker selló **`b3:715237c78edd…`** — **el mismo hash que
194`build-state.json` traía del otro hub.** Dos máquinas distintas, bytes idénticos: reproducibilidad
195confirmada de paso, que no era el objetivo de la prueba.
196
197Binario: ELF x86-64 **estático** (sin loader), `brush 0.4.0`, **7,25 MB** — contra **1,23 MB** del
198busybox que trae los 401 applets. Botar busybox por brush, hoy, **engorda** la imagen.
199
200## Lo que PASA
201
202| prueba | brush | control busybox |
203|---|---|---|
204| **Banco POSIX** — 57 casos (`scripts/sh-banco-posix.sh`) | **56/57** | 56/57 |
205| **Las 1130 fases de shell del corpus** (`sh -n` sobre las fases de las 549 recetas que las tienen) | **0 rechazadas** | 0 rechazadas |
206| **`configure` de ffmpeg** — 8345 líneas, `sh -n` | rc=0 | rc=0 |
207| **Invocado como `sh`** (argv[0]) | ✅ | ✅ |
208| **El patrón de las cards de arje** (`sh -c '…; exec …'`) | ✅ | ✅ |
209
210La única divergencia del banco: **`shift` más allá de `$#` devuelve 2**, y POSIX/busybox/bash
211devuelven 1. Cosmético, pero un `set -e` que haga `shift` de más se comporta distinto.
212
213⚠ **Y el banco NO es prueba de suficiencia** — esto es lo que hay que recordar del ejercicio:
214**brush lo pasa entero y aun así no construye una sola receta autotools.** Un banco de casos
215sintéticos mide lo que a uno se le ocurre preguntar; el consumidor real mide lo que hay.
216
217## Lo que FALLA
218
219**Tres recetas autotools reales, con el único cambio de `/bin/sh` en una copia del lab:**
220
221```sh
222cp -al /opt/takana/.dev-fs/alpine  $L/alpine       # copia con hardlinks, el lab compartido intacto
223cp   …-brush/usr/bin/brush         $L/alpine/bin/brush
224ln -sfn /bin/brush                 $L/alpine/bin/sh   # ← lo ÚNICO que cambia; sed/grep/awk siguen siendo busybox
225flock -o work/.farm-build.lock env TAKANA_ROOTFS=$L/alpine \
226  ./target/release/takana --store <store-tirable> build recipes/<r>.toml
227```
228
229| receta | control (busybox sh) | brush |
230|---|---|---|
231| `flex` | ✅ sellada | ❌ `make[1]: Error 2` |
232| `pkgconf` | ✅ sellada | ❌ `./libtool: syntax error at line 368 col 30` |
233| `oniguruma` | ✅ sellada | ❌ `make[1]: Error 2` |
234
235### La causa, hasta donde está medida
236
237**El `configure` corre entero y bien bajo brush.** Lo que sale mal es el `libtool` que ese
238`configure` GENERA. Regenerado el mismo fichero con cada shell y comparado, **25 líneas difieren, y
239todas en el mismo sentido: falta un nivel de escapado.**
240
241```
242                        control (busybox)                     brush
243línea 62    ECHO="printf %s\\n"                      ECHO="printf %s\n"
244línea 132   file_magic_cmd="\$MAGIC_CMD"             file_magic_cmd="$MAGIC_CMD"
245línea 176   …'s/^.*[ ]\\([ABCDGIRSTW]…               …'s/^.*[ ]\([ABCDGIRSTW]…
246```
247
248Ese fichero ya no lo parsea **nadie** — ni brush ni busybox lo aceptan (`rc=2` los dos, misma línea
249368). Y el contraprueba cierra el caso: **el `libtool` del control lo parsea brush sin quejarse
250(`rc=0`)**. ⇒ El parser de brush está bien; lo que difiere es lo que su shell PRODUCE.
251
252### El mecanismo, encontrado (2026-09-21, después de escribir lo de arriba)
253
254Primero se escribió acá que la causa no estaba determinada, tras descartar las tres sospechas
255obvias —`echo` con barras invertidas, sustitución de comando preservando `\\`, y el
256`func_quote_for_eval` de libtool aplicado a mano—, que dan **idéntico** en ambos shells. La causa
257apareció bajando al `config.status` que genera el `libtool`. **Es de una línea:**
258
259```sh
260V=hola
261echo "a: [`echo \\$V`]"     # POSIX: $V      · brush: \hola
262echo "b: [`echo \\\\`]"     # POSIX: \       · brush: \\
263echo "c: [`echo \$V`]"      # POSIX: hola    · brush: $V
264echo "d: [$(echo \\$V)]"    # POSIX: \hola   · brush: \hola   ✅
265```
266
267**brush aplica a las comillas invertidas las reglas de `$( )`.** POSIX manda otra cosa: dentro de
268`` `…` `` la barra invertida conserva su significado literal **salvo** cuando precede a `$`,
269`` ` `` o `\`, donde se ELIMINA antes de parsear el texto del comando (POSIX.1-2024, 2.6.3). Que el
270caso **d** sí esté bien es lo que delata la causa.
271
272Por qué rompe libtool: el `config.status` generado trae este bucle, que decide si cada variable
273necesita escapado antes de escribirla en el `libtool`:
274
275```sh
276for var in SHELL ECHO PATH_SEPARATOR SED GREP …; do
277    case `eval \\$ECHO \\""\\$$var"\\"` in
278    *[\\\`\"\$]*)  eval "lt_$var=\\\"\`\$ECHO \"\$$var\" | \$SED \"\$sed_quote_subst\"\`\\\"" ;;
279    *)             eval "lt_$var=\\\"\$$var\\\"" ;;
280    esac
281done
282```
283
284Bajo brush ese `case` evalúa a **cadena vacía**, así que **todas** las variables caen en la rama sin
285escapar. De ahí las 25 líneas y de ahí que el `configure` termine bien: no falla nada, sólo sale mal
286escrito.
287
288**Reportado upstream: [reubeno/brush#1394](https://github.com/reubeno/brush/issues/1394).**
289
290## Y el otro dato, que no es un fallo pero decide
291
292**Latencia de arranque: 813 µs contra 91 µs de busybox — 8,9×** (300 invocaciones de `sh -c :`).
293Un `configure` de autotools levanta decenas de miles de shells: a ese factor, el sandbox entero se
294vuelve medible más lento. Es un argumento independiente del bug, y no se arregla arreglando el bug.
295
296## Qué queda de esto
297
2981. **brush NO entra al sandbox de build.** El frente A sigue con busybox hasta que esto se cierre.
2992. **El frente del producto sigue abierto**: las cards, la getty y la shell interactiva no generan
300   libtool. Ahí brush no tiene ninguna evidencia en contra — pero tampoco se probó, y el
301   `product-boot-test.sh` es la prueba que falta.
3023. **Reportado upstream: [reubeno/brush#1394](https://github.com/reubeno/brush/issues/1394)**, con
303   el reproductor de una línea, la tabla de los cuatro casos y el fragmento de `config.status`.
3044. **El paso 5 del plan (grep/sed POSIX en Rust) sube de prioridad**, porque no depende del shell.
3055. **No se tocó el lab compartido**: copia con hardlinks y stores tirables. El worker siguió
306   compilando su cola en paralelo, serializado por el `flock`.
307
308## ¿Parchar, o buscar otra opción?
309
310**Primero lo que cierra la pregunta barata: adelantar el pin NO sirve.** Nuestra receta apunta a
311`96a26d0c` (tag `brush-shell-v0.4.0`, mayo 2026), que sigue siendo el último release. Se construyó
312**`main` de hoy** (`737dd57e`, 2026-09-21 — cuatro meses y medio de trabajo encima) y **reproduce
313idéntico**, los cuatro casos. No hay nada que esperar de la rama.
314
315Lo que queda, en orden de coste:
316
317| opción | qué cuesta | qué deja |
318|---|---|---|
319| **Esperar a #1394** | cero | el upstream es activísimo (issue #1393 de anteayer) y el bug es acotado, pero no es nuestro calendario |
320| **Parchar brush en la receta** | un `.patch` en el árbol, como ya hacen 13 recetas | ⚠ es un parche **al parser de un shell**, que es el componente más cargado de todos. Y el bug apareció en el PRIMER build real: no es prueba de que sea el único |
321| **dash** | receta nueva, C, ~200 KB. Es el `sh` de referencia POSIX | resuelve el sandbox YA — y no avanza nada la soberanía en Rust |
322| **bash como `/bin/sh` del producto** | cero: ya está sellada Y en los 7 perfiles | tapa el frente B sin escribir nada. No sirve para el sandbox sin medirlo antes |
323| **Dejar busybox donde está** | cero | es lo que dice el plan: el shell NO es por donde se empieza |
324
325**La recomendación es la última, y no por conservadurismo.** El plan ya ordenaba los pasos 1, 2 y 5
326—recortar el defconfig, declarar lo sellado, escribir `grep`/`sed` POSIX— **sin depender del shell**,
327y valen 129 applets y dos piezas de userland. Gastar el turno en pelear el shell es empezar por el
328único frente que está bloqueado por terceros.
329
330Lo que sí conviene hacer ya, porque es gratis: **suscribirse a #1394** y **medir brush como `/bin/sh`
331del PRODUCTO** (`product-boot-test.sh` + las cards), que es un consumidor que no genera libtool y
332donde brush no tiene ninguna evidencia en contra.

plan-botar-busybox.md

Plan — botar busybox, MEDIDO

Fecha: 2026-09-21 · Instrumento: los 401 symlinks de applet del artefacto sellado 2a2b1280…-busybox (contados en el store del worker), cruzados con el estado y la membresía de perfil de cada receta en docs/state/build-state.json y con grep '[deps]' recipes/*.toml. Tabla applet-por-applet: docs/state/busybox-applets.tsv (401 filas, generada, no escrita a mano).

Por qué existe este documento

La pregunta fue «¿qué hay que hacer para botar busybox?», con la sospecha de que faltaba escribir mucho userland en Rust. El número dice lo contrario, y el hueco está en otro lado:

De los 401 applets, 148 ya tienen dueño sellado Y en perfil (el applet es redundante en la imagen), 125 tienen dueño sellado que NO viaja en ninguna imagen (trabajo hecho y no cobrado), y de los 128 sin dueño, 105 son basura que se borra. El trabajo de escritura real son 23 applets repartidos en 8 piezas.

Es la lección de foot y del §D de plan-barrido-perfiles-y-servicios.md otra vez: uutils, findutils, findutils-xargs, diffutils y brush están todos sealed con perfiles: []. Se construyeron, se sellaron, y ninguna imagen los lleva.

Son TRES frentes, no uno — y esto es lo que hace que «botar busybox» se lea como imposible

Mezclarlos es el error que cuesta el trabajo. Tienen dificultad, dueño y riesgo distintos:

frentequé es busybox ahídificultad
A · Sandbox de build[deps] build = [… "busybox" …] en 24 recetas — es el sh+coreutils que el sandbox del lab trae (recipes/jq.toml:5-7, recipes/zlib-ng.toml:11). Más el /toolchain mixto de Alpine con el swap de docs/11-bootstrap.md:234-240media — no es runtime, no toca el producto
B · Runtime del productolos applets de /bin y /sbin de las imágenes. USERLAND_COMPONENTS (crates/takana-bootstrap/src/lib.rs:406) ya los ensombrece con los binarios Rust y retira el homónimo de los otros dirs (lib.rs:676)baja — la máquina ya está construida, falta munición
C · Stage 1 / self-host / atestaciónSTAGE1_COMPONENTS (lib.rs:172, el 4/4), ATTEST_PATHS como raíz-de-confianza (lib.rs:465), y el exec de las cards de getty y sshd (lib.rs:429, :240)alta — es el ADR 0008, y toca la evidencia de reproducibilidad of_tree=9adefb82…

⚠ La membresía de perfil de busybox es del frente A, no del B. busybox figura en cli, servidor, los cuatro escritorios y metal-tigerlake, pero no está declarado como raíz en docs/state/targets.toml (los únicos hits ahí son comentarios). Llega por la clausura, porque 24 recetas lo declaran build-dep: abseil-cpp chrony cronie crun dwarves firmware-tigerlake ia-modelo-embeddings intel-ucode jq libmnl libnftnl linux linux-firmware llama-cpp logrotate nftables opensmtpd popt protobuf qdrant sof-firmware valkey wireless-regdb zlib-ng. Sacarlo de las imágenes empieza por ahí y no requiere escribir ni una línea de userland.

El reparto de los 401

estado del dueñonqué significa
sellada + en perfil148el reemplazo ya viaja en la imagen: el applet es redundante hoy
sellada, sin perfil125construido, sellado, y no viaja: falta declararlo
sin dueño128de los cuales 105 son BORRAR y 23 son trabajo real

148 · Ya cubiertos y en la imagen — el applet sobra

util-linux 48 · iproute2 19 · procps-ng 12 · shadow 14 · kbd 12 · xz 6 · iputils 5 · e2fsprogs 5 · bzip2 3 · cronie 3 · dhcpcd 3 · ncurses 2 · dosfstools 2 · chrony 2 · vim 2 · less lsof pciutils usbutils mandoc socat patch binutils tar wget 1 c/u.

⚠ 24 de estos 148 NO son drop-in, y están marcados en el TSV. El caso grande es la red: iproute2 da ip/ss, no ifconfig/route/arp/netstat — eso es net-tools, que no está en el corpus. Igual con addgroup/adduser (shadow da groupadd/useradd), udhcpc (dhcpcd es otro cliente con otro contrato de hooks) y nc (socat tiene otra sintaxis). Cada uno de esos 24 es una migración de llamadores, no un borrado. Mirar quién los invoca en scripts/ y en las cards antes.

125 · Sellados y sin viajar — la munición que ya está fabricada

dueñonqué cubre
uutils90todo el núcleo POSIX: ls cp mv rm cat head tail sort cut tr wc stat date dd chmod chown …
arje-zero20init linuxrc runlevel halt poweroff reboot run-init switch_root + los 12 de runit (runsv sv svc svlogd chpst envdir setuidgid softlimit start-stop-daemon …)
hammerd4syslogd klogd logger logread
brush3sh ash hush
gzip3gzip gunzip zcat
diffutils2diff cmp
findutils / findutils-xargs / libarchive3find · xargs · cpio

Los 24 de arje-zero y hammerd no cuestan nada: el reemplazo ya corre en el producto. Son applets instalados que nadie ejecuta. Salen del defconfig y se prueba que la imagen arranque igual.

128 · Sin dueño

105 se borran (no se reemplazan, salen del defconfig): dpkg dpkg-deb rpm rpm2cpio httpd ftpd ftpget ftpput tftp tftpd telnet telnetd inetd dnsd sendmail popmaildir lpd lpq lpr ubi* nand* nbd-client i2c* hdparm fdformat setserial powertop bootchartd fbsplash conspy ether-wake udhcpd udpsvd tcpsvd watchdog whois tree which unzip … (lista completa en el TSV).

23 son el trabajo real:

piezaappletsqué es
kmodinsmod rmmod lsmod modprobe depmod modinfo (6)no hay receta kmod en el corpus. En Rust: finit_module + parseo de modules.dep. Bien acotado
psmisckillall killall5 pstree fuser (4)procps-ng NO los trae. Traer receta psmisc (C) o escribir
grep POSIXgrep egrep fgrep (3)grep GNU está sellado (sin perfil) como puente. El Rust nativo se escribe sobre grep-matcher/grep-regex/grep-searcher
gestor de /devmdev makedevs uevent (3)decisión: lo absorbe arje, o entra una receta eudev/mdevd
bcbc dc (2)traer receta
gettygetty cttyhack (2)hoy la card de getty ejecuta /bin/busybox (lib.rs:429)
awkawk (1)decisión abierta, ver abajo
sedsed (1)sed GNU sellado y en los 7 perfiles; el Rust nativo está por escribir
dnsnslookup (1)traer receta bind-tools/doggo

Las tres clases de reemplazo — y cuándo forkear (nunca)

La pregunta era si hay que forkear los que «existen pero no son exactos». No, y hay tres clases distintas que conviene no mezclar:

1 · Adopción exacta. uutils/coreutils, uutils/findutils, uutils/diffutils: reimplementación madura, MIT, receta Cargo con commit pinneado (ADR 0006), multicall. El patrón está probado y documentado en la cabecera de recipes/uutils.toml. Lo que falta de esta clase es uutils/procps y uutils/util-linux — receta calcada, coste bajo. ⚠ Verificar su estado upstream antes de prometer los 60 applets que cubrirían: util-linux en Rust era embrionario.

2 · «Existe pero no es exacto» — y forkear es la trampa. ripgrep no es grep POSIX, sd no es sed, fd no es find (por eso la receta buena es uutils/findutils y no fd), procs no es ps, bottom no es top, helix no es vi. Todas están selladas y en los 7 perfiles, y ninguna sirve de reemplazo: cambian las banderas. Forkear ripgrep para que hable POSIX es peor que escribir — heredás su arquitectura de recursión+ignore y peleás contra ella para siempre. Lo correcto es usarlas como librerías: grep-matcher/grep-regex/grep-searcher están publicadas justamente para eso, y un grep POSIX encima son unos cientos de líneas.

3 · awk es el caso feo, y hay que decidirlo, no resolverlo de paso. recipes/goawk.toml existe y está sellada, pero goawk es Go, no Rust — no cumple la premisa de soberanía de cadena de la Etapa C. frawk es Rust pero es otro dialecto (JIT, semántica propia). gawk es C con extensiones GNU que los configure del corpus sí usan. Las tres opciones son legítimas y ninguna es gratis.

El bloqueante, y la buena noticia

Sin sh POSIX no se bota busybox en ningún frente: es el exec de las cards, el /bin/sh del producto y el shell del sandbox. Lo esperado era que fuera un proyecto de meses.

recipes/brush.toml (0.4.0, reubeno/brush, MIT, Rust, compat bash) está sealed. Compila estático musl con zig cc y produce /usr/bin/brush.

⚠ Y está sin validar como sh. La receta es un import crudo de nixpkgs y lo dice en su primera línea: «PUNTO DE PARTIDA, no final». No está en ningún perfil, nadie lo ejecutó como /bin/sh, y el sandbox de build es el consumidor más duro que existe (configure de autotools). La unidad de trabajo #1 es medirlo, no adoptarlo.

Orden de ataque

Cada paso es una unidad de trabajo con su criterio de terminado. Los tres primeros no tocan el frente C y se pueden hacer hoy.

  1. Recortar el defconfig. Sacar los 105 de BORRAR + los 24 que arje-zero/hammerd ya reemplazan = 129 applets menos, cero código escrito. Terminado cuando la imagen arranca, el E2E de QEMU pasa y find . -type l | wc -l baja de 401 a ~272.
  2. Declarar la munición que ya existe. uutils, findutils, findutils-xargs, diffutils, brush, gzip, libarchive a los perfiles de targets.toml. Terminado cuando build-state.py los marca con perfil y la imagen los lleva.
  3. Medir brush como /bin/sh. HECHO, 2026-09-21 — veredicto abajo: no sirve todavía para el sandbox. Queda pendiente medirlo como /bin/sh del PRODUCTO (cards + product-boot-test.sh), que es un consumidor distinto y mucho más blando.
  4. Las dos recetas de adopción baratas (uutils/procps, uutils/util-linux), previa verificación de madurez upstream. Cubren ~60 applets de los 148 «ya cubiertos por C».
  5. grep y sed POSIX en Rust. Es la escritura de mejor relación coste/beneficio y desbloquea el /toolchain del frente A.
  6. Migrar los 24 build-deps. Cambiar busybox por el conjunto real en [deps] build de las 24 recetas. Terminado cuando busybox sale de los 7 perfiles.
  7. La cola larga: kmod, psmisc, gestor de /dev, getty, bc, dns. Después del shell, no antes.
  8. ADR — awk, y ADR — el frente C (sustituir busybox en STAGE1_COMPONENTS y ATTEST_PATHS). No antes de que 1–7 estén cerrados.

La métrica, y el guardián que falta

docs/state/busybox-applets.tsv convierte esto en un número que baja: 401 → 272 → … → 0. Regenerarlo es contar los symlinks del artefacto sellado y cruzarlos con build-state.json.

Falta un guardián, y sin él el paso 1 se deshace solo: un script que falle si alguna receta, script o card invoca un applet que ya salió del defconfig. Hoy nada lo detectaría — el fallo aparecería en boot, en otra máquina, semanas después.

Lo que este documento NO midió (y hay que decirlo así)


Veredicto del paso 3 — brush como /bin/sh, MEDIDO

Fecha: 2026-09-21 · Dónde: dev.gioser.net, todo bajo flock -o work/.farm-build.lock · Contra qué: busybox 1.36.1 sh (ash) como control, en la misma caja y la misma corrida.

No sirve todavía como /bin/sh del SANDBOX DE BUILD. Pasa el banco POSIX igual que busybox, parsea todo el corpus, y aun así 0 de 3 recetas autotools construyen (control: 3/3). Lo que falla no es su parser: es que el libtool que su configure genera sale con un nivel de escapado de menos y después no lo parsea ningún shell, ni él ni busybox.

Lo que se construyó

takana build recipes/brush.toml en el worker selló b3:715237c78edd… — el mismo hash que build-state.json traía del otro hub. Dos máquinas distintas, bytes idénticos: reproducibilidad confirmada de paso, que no era el objetivo de la prueba.

Binario: ELF x86-64 estático (sin loader), brush 0.4.0, 7,25 MB — contra 1,23 MB del busybox que trae los 401 applets. Botar busybox por brush, hoy, engorda la imagen.

Lo que PASA

pruebabrushcontrol busybox
Banco POSIX — 57 casos (scripts/sh-banco-posix.sh)56/5756/57
Las 1130 fases de shell del corpus (sh -n sobre las fases de las 549 recetas que las tienen)0 rechazadas0 rechazadas
configure de ffmpeg — 8345 líneas, sh -nrc=0rc=0
Invocado como sh (argv[0])✅✅
El patrón de las cards de arje (sh -c '…; exec …')✅✅

La única divergencia del banco: shift más allá de $# devuelve 2, y POSIX/busybox/bash devuelven 1. Cosmético, pero un set -e que haga shift de más se comporta distinto.

⚠ Y el banco NO es prueba de suficiencia — esto es lo que hay que recordar del ejercicio: brush lo pasa entero y aun así no construye una sola receta autotools. Un banco de casos sintéticos mide lo que a uno se le ocurre preguntar; el consumidor real mide lo que hay.

Lo que FALLA

Tres recetas autotools reales, con el único cambio de /bin/sh en una copia del lab:

cp -al /opt/takana/.dev-fs/alpine  $L/alpine       # copia con hardlinks, el lab compartido intacto
cp   …-brush/usr/bin/brush         $L/alpine/bin/brush
ln -sfn /bin/brush                 $L/alpine/bin/sh   # ← lo ÚNICO que cambia; sed/grep/awk siguen siendo busybox
flock -o work/.farm-build.lock env TAKANA_ROOTFS=$L/alpine \
  ./target/release/takana --store <store-tirable> build recipes/<r>.toml
recetacontrol (busybox sh)brush
flex✅ sellada❌ make[1]: Error 2
pkgconf✅ sellada❌ ./libtool: syntax error at line 368 col 30
oniguruma✅ sellada❌ make[1]: Error 2

La causa, hasta donde está medida

El configure corre entero y bien bajo brush. Lo que sale mal es el libtool que ese configure GENERA. Regenerado el mismo fichero con cada shell y comparado, 25 líneas difieren, y todas en el mismo sentido: falta un nivel de escapado.

                        control (busybox)                     brush
línea 62    ECHO="printf %s\\n"                      ECHO="printf %s\n"
línea 132   file_magic_cmd="\$MAGIC_CMD"             file_magic_cmd="$MAGIC_CMD"
línea 176   …'s/^.*[ ]\\([ABCDGIRSTW]…               …'s/^.*[ ]\([ABCDGIRSTW]…

Ese fichero ya no lo parsea nadie — ni brush ni busybox lo aceptan (rc=2 los dos, misma línea 368). Y el contraprueba cierra el caso: el libtool del control lo parsea brush sin quejarse (rc=0). ⇒ El parser de brush está bien; lo que difiere es lo que su shell PRODUCE.

El mecanismo, encontrado (2026-09-21, después de escribir lo de arriba)

Primero se escribió acá que la causa no estaba determinada, tras descartar las tres sospechas obvias —echo con barras invertidas, sustitución de comando preservando \\, y el func_quote_for_eval de libtool aplicado a mano—, que dan idéntico en ambos shells. La causa apareció bajando al config.status que genera el libtool. Es de una línea:

V=hola
echo "a: [`echo \\$V`]"     # POSIX: $V      · brush: \hola
echo "b: [`echo \\\\`]"     # POSIX: \       · brush: \\
echo "c: [`echo \$V`]"      # POSIX: hola    · brush: $V
echo "d: [$(echo \\$V)]"    # POSIX: \hola   · brush: \hola   ✅

brush aplica a las comillas invertidas las reglas de $( ). POSIX manda otra cosa: dentro de `…` la barra invertida conserva su significado literal salvo cuando precede a $, ` o \, donde se ELIMINA antes de parsear el texto del comando (POSIX.1-2024, 2.6.3). Que el caso d sí esté bien es lo que delata la causa.

Por qué rompe libtool: el config.status generado trae este bucle, que decide si cada variable necesita escapado antes de escribirla en el libtool:

for var in SHELL ECHO PATH_SEPARATOR SED GREP …; do
    case `eval \\$ECHO \\""\\$$var"\\"` in
    *[\\\`\"\$]*)  eval "lt_$var=\\\"\`\$ECHO \"\$$var\" | \$SED \"\$sed_quote_subst\"\`\\\"" ;;
    *)             eval "lt_$var=\\\"\$$var\\\"" ;;
    esac
done

Bajo brush ese case evalúa a cadena vacía, así que todas las variables caen en la rama sin escapar. De ahí las 25 líneas y de ahí que el configure termine bien: no falla nada, sólo sale mal escrito.

Reportado upstream: reubeno/brush#1394.

Y el otro dato, que no es un fallo pero decide

Latencia de arranque: 813 µs contra 91 µs de busybox — 8,9× (300 invocaciones de sh -c :). Un configure de autotools levanta decenas de miles de shells: a ese factor, el sandbox entero se vuelve medible más lento. Es un argumento independiente del bug, y no se arregla arreglando el bug.

Qué queda de esto

  1. brush NO entra al sandbox de build. El frente A sigue con busybox hasta que esto se cierre.
  2. El frente del producto sigue abierto: las cards, la getty y la shell interactiva no generan libtool. Ahí brush no tiene ninguna evidencia en contra — pero tampoco se probó, y el product-boot-test.sh es la prueba que falta.
  3. Reportado upstream: reubeno/brush#1394, con el reproductor de una línea, la tabla de los cuatro casos y el fragmento de config.status.
  4. El paso 5 del plan (grep/sed POSIX en Rust) sube de prioridad, porque no depende del shell.
  5. No se tocó el lab compartido: copia con hardlinks y stores tirables. El worker siguió compilando su cola en paralelo, serializado por el flock.

¿Parchar, o buscar otra opción?

Primero lo que cierra la pregunta barata: adelantar el pin NO sirve. Nuestra receta apunta a 96a26d0c (tag brush-shell-v0.4.0, mayo 2026), que sigue siendo el último release. Se construyó main de hoy (737dd57e, 2026-09-21 — cuatro meses y medio de trabajo encima) y reproduce idéntico, los cuatro casos. No hay nada que esperar de la rama.

Lo que queda, en orden de coste:

opciónqué cuestaqué deja
Esperar a #1394ceroel upstream es activísimo (issue #1393 de anteayer) y el bug es acotado, pero no es nuestro calendario
Parchar brush en la recetaun .patch en el árbol, como ya hacen 13 recetas⚠ es un parche al parser de un shell, que es el componente más cargado de todos. Y el bug apareció en el PRIMER build real: no es prueba de que sea el único
dashreceta nueva, C, ~200 KB. Es el sh de referencia POSIXresuelve el sandbox YA — y no avanza nada la soberanía en Rust
bash como /bin/sh del productocero: ya está sellada Y en los 7 perfilestapa el frente B sin escribir nada. No sirve para el sandbox sin medirlo antes
Dejar busybox donde estáceroes lo que dice el plan: el shell NO es por donde se empieza

La recomendación es la última, y no por conservadurismo. El plan ya ordenaba los pasos 1, 2 y 5 —recortar el defconfig, declarar lo sellado, escribir grep/sed POSIX— sin depender del shell, y valen 129 applets y dos piezas de userland. Gastar el turno en pelear el shell es empezar por el único frente que está bloqueado por terceros.

Lo que sí conviene hacer ya, porque es gratis: suscribirse a #1394 y medir brush como /bin/sh del PRODUCTO (product-boot-test.sh + las cards), que es un consumidor que no genera libtool y donde brush no tiene ninguna evidencia en contra.