Se ensamblaron dos Stage 1 completos y se arrancaron los dos, para que cualquier diferencia se atribuya al recorte y no al azar: base b3:1a3823b4... 400 applets qemu rc=0 recortado b3:3aa8cd2d... 278 applets qemu rc=0 Los dos dan shell y se apagan limpio. El diff de las dos consolas son 35 líneas y ninguna es funcional: TSC, direcciones del RAMDISK, BogoMIPS, hora del RTC. La única diferencia real es el conteo de applets desde dentro de la imagen: 410 contra 288. Verificado en el recortado: arje-zero como PID 1, la seed card carga (la getty llegó a incarnar), getty -l /bin/sh da prompt y responde, y poweroff -f apaga de verdad -- uno de los cinco indultados, probado en vivo. Corrido en el worker con QEMU en TCG: el LXC no expone /dev/kvm. Hubo que instalar qemu-system-x86-core y reingerir la semilla de Stage 0, que no estaba en ese store y salió con el mismo hash que el manifiesto ya anotaba. Es Stage 1, no el producto: product-boot-test.sh sigue pendiente porque al store del worker le faltan netup, openssh, uutils, findutils-xargs, diffutils y ripgrep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
457 lines
27 KiB
Markdown
457 lines
27 KiB
Markdown
# 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:
|
||
|
||
| frente | qué 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-240` | media — no es runtime, no toca el producto |
|
||
| **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** |
|
||
| **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…`** |
|
||
|
||
⚠ **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ño | n | qué significa |
|
||
|---|---|---|
|
||
| **sellada + en perfil** | 148 | el reemplazo ya viaja en la imagen: el applet es redundante hoy |
|
||
| **sellada, sin perfil** | 125 | construido, sellado, y no viaja: falta declararlo |
|
||
| **sin dueño** | 128 | de 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ño | n | qué cubre |
|
||
|---|---|---|
|
||
| `uutils` | 90 | todo el núcleo POSIX: `ls cp mv rm cat head tail sort cut tr wc stat date dd chmod chown …` |
|
||
| `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 …`) |
|
||
| `hammerd` | 4 | `syslogd klogd logger logread` |
|
||
| `brush` | 3 | **`sh ash hush`** |
|
||
| `gzip` | 3 | `gzip gunzip zcat` |
|
||
| `diffutils` | 2 | `diff cmp` |
|
||
| `findutils` / `findutils-xargs` / `libarchive` | 3 | `find` · `xargs` · `cpio` |
|
||
|
||
⚠ **CORRECCIÓN (2026-09-21, al ejecutar el paso 1): los 24 de `arje-zero` y `hammerd` NO son
|
||
gratis, y esta tabla decía mal.** Esas dos recetas reemplazan la **función**, no el **comando**:
|
||
cada una instala UN binario (`-p arje-zero`, `-p hammerd`), así que no existe un `poweroff`, un
|
||
`logger` ni un `setuidgid` que las respalde. «Cubierto» significaba «nadie necesita correr esto»,
|
||
y para 5 de los 24 era falso. Ver los indultos del paso 1.
|
||
|
||
### 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:**
|
||
|
||
| pieza | applets | qué es |
|
||
|---|---|---|
|
||
| **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 |
|
||
| **psmisc** | `killall killall5 pstree fuser` (4) | **`procps-ng` NO los trae.** Traer receta psmisc (C) o escribir |
|
||
| **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` |
|
||
| **gestor de /dev** | `mdev makedevs uevent` (3) | decisión: lo absorbe arje, o entra una receta eudev/mdevd |
|
||
| **bc** | `bc dc` (2) | traer receta |
|
||
| **getty** | `getty cttyhack` (2) | hoy la card de getty ejecuta `/bin/busybox` (`lib.rs:429`) |
|
||
| **awk** | `awk` (1) | decisión abierta, ver abajo |
|
||
| **sed** | `sed` (1) | `sed` GNU sellado y en los 7 perfiles; el Rust nativo está por escribir |
|
||
| **dns** | `nslookup` (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`.**~~ **HECHO, 2026-09-21 — 401 → 277 applets. Veredicto al final.**
|
||
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í)
|
||
|
||
- **El reparto applet→destino es juicio, no medida.** Lo medido son los 401 symlinks, el estado
|
||
`sealed`/perfiles de cada receta y las 24 recetas con `busybox` en `[deps] build`. La asignación de
|
||
dueño la escribí yo y hay que revisarla receta a receta.
|
||
- **No se verificó la equivalencia de banderas de ninguna pareja.** Los 24 marcados `NO-drop-in` son
|
||
los que sé que difieren; el resto dice `sin-verificar` y eso es literal, no una forma de decir «va
|
||
a andar».
|
||
- **No se midió qué applets usa el producto de verdad.** El `defconfig` instala 401 porque nadie
|
||
eligió; cuáles se ejecutan en una imagen viva no está medido. Eso abarataría el paso 1.
|
||
- **`uutils/procps` y `uutils/util-linux` no se verificaron upstream.** Los ~60 applets que les
|
||
atribuyo son una hipótesis de trabajo.
|
||
|
||
---
|
||
|
||
# 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
|
||
|
||
| prueba | brush | control busybox |
|
||
|---|---|---|
|
||
| **Banco POSIX** — 57 casos (`scripts/sh-banco-posix.sh`) | **56/57** | 56/57 |
|
||
| **Las 1130 fases de shell del corpus** (`sh -n` sobre las fases de las 549 recetas que las tienen) | **0 rechazadas** | 0 rechazadas |
|
||
| **`configure` de ffmpeg** — 8345 líneas, `sh -n` | rc=0 | rc=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:**
|
||
|
||
```sh
|
||
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
|
||
```
|
||
|
||
| receta | control (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:**
|
||
|
||
```sh
|
||
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`:
|
||
|
||
```sh
|
||
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](https://github.com/reubeno/brush/issues/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](https://github.com/reubeno/brush/issues/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ón | qué cuesta | qué deja |
|
||
|---|---|---|
|
||
| **Esperar a #1394** | cero | el upstream es activísimo (issue #1393 de anteayer) y el bug es acotado, pero no es nuestro calendario |
|
||
| **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 |
|
||
| **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 |
|
||
| **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 |
|
||
| **Dejar busybox donde está** | cero | es 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.
|
||
|
||
|
||
---
|
||
|
||
# Paso 1 — el recorte, HECHO y MEDIDO
|
||
|
||
**Fecha:** 2026-09-21 · **Dónde:** `dev.gioser.net`, bajo `flock -o`, contra un store tirable y con
|
||
un directorio de recetas por hardlinks — el `recipes/` del worker no se tocó.
|
||
|
||
> **401 → 277 applets.** 124 retirados, 5 indultados, binario de **1.230.976 → 977.096 bytes**
|
||
> (−20,6 %). Nuevo hash `b3:a24fdbf6…` (el vigente era `2a2b1280…`). Y **3 de las 24 recetas que
|
||
> declaran busybox build-dep se reconstruyeron y sellaron con el recortado**: el sandbox sigue en pie.
|
||
|
||
## Lo que el ejercicio corrigió del plan
|
||
|
||
**Mi tabla decía que los 24 applets de `arje-zero`/`hammerd` eran gratis. Es falso para 5 de ellos.**
|
||
Esas recetas instalan **un binario cada una** (`-p arje-zero`, `-p hammerd`): reemplazan la FUNCIÓN,
|
||
no el COMANDO. No hay un `poweroff` ni un `setuidgid` detrás. Los cinco indultos, con su razón:
|
||
|
||
| indultado | por qué se queda |
|
||
|---|---|
|
||
| `setuidgid` | la card de **gitea** hace `setuidgid gitea` (`recipes/gitea.toml:89`, «applet de busybox, comprobado presente»). Sin él el servicio arranca, muere y reintenta para siempre |
|
||
| `switch_root` | el `/init` del initramfs del instalador lo ejecuta (`scripts/takana-live-install.sh:375`), **antes** de que arje exista |
|
||
| `poweroff` · `halt` · `reboot` | **nada más en el corpus apaga la máquina.** arje-zero es PID 1, no una CLI. Quitarlos deja un producto que no se puede apagar desde la shell |
|
||
|
||
Los tres primeros los encontró el vigía; los tres últimos, preguntarse qué instala arje-zero de
|
||
verdad en vez de creerle a mi propia tabla.
|
||
|
||
## El vigía — y por qué un vigía ingenuo no sirve
|
||
|
||
`scripts/busybox-vigia.py` falla si alguna receta, script o card invoca un applet retirado. **Lee la
|
||
lista de la propia receta** (`RETIRADOS='…'`) cruzada con la columna `simbolo_kconfig` del TSV, así
|
||
que no hay dos listas que se desincronicen.
|
||
|
||
Su primera versión daba **91 hits y ninguno real**. Las cuatro reglas que lo bajaron a 0, todas
|
||
medidas contra falsos positivos concretos:
|
||
|
||
| regla | qué mataba |
|
||
|---|---|
|
||
| quitar comentarios antes de buscar | `# busybox provee sendmail, no mail` (logrotate), `/// los dos fsync no son prolijidad` |
|
||
| en Rust/Python, mirar sólo cadenas **que parezcan una línea de comandos** | `"init"` como nombre de capacidad, `tree` como variable, `rx = re.compile(…)` |
|
||
| ignorar nombres que el propio script define como función | `$(ts)` en los seis scripts de `scripts/farm/` |
|
||
| ignorar patrones de `case` y `command -v X` | `proc\|sys\|dev\|init) continue ;;` · `command -v dpkg` en el censo de la mudanza |
|
||
|
||
Y `[` / `[[` no se vigilan: son palabras del shell, y todo script bash del hub los usa sin que eso
|
||
sea una llamada a un applet.
|
||
|
||
⚠ **El vigía mira ESTE repo.** No ve los `configure` de terceros que corren dentro del sandbox, que
|
||
son quienes podrían llamar a `which` o `unzip`. Por eso la verificación de abajo no es opcional.
|
||
|
||
## El mapa applet → símbolo NO se deriva por mayúsculas
|
||
|
||
Sale de las líneas `//applet:` del fuente de busybox 1.36.1 y vive en la columna `simbolo_kconfig`
|
||
del TSV. Poner el nombre en mayúsculas **falla en 6 casos medidos**: `[`→`TEST1`, `[[`→`TEST2`,
|
||
`arch`→`BB_ARCH`, `nbd-client`→`NBDCLIENT`, `sh`→`SH_IS_ASH`, `sysctl`→`BB_SYSCTL`. Dos de esos seis
|
||
estaban en la lista a retirar.
|
||
|
||
## Verificación
|
||
|
||
| qué | resultado |
|
||
|---|---|
|
||
| applets tras el recorte | **277** (401 − 124), exactamente los previstos |
|
||
| los 5 indultados presentes | ✅ y también `sh` y `getty` |
|
||
| los que debían irse | ✅ `httpd telnetd dpkg rpm init syslogd runsv tree which unzip` fuera |
|
||
| **el sandbox sigue construyendo** | ✅ `zlib-ng`, `jq` y `logrotate` reconstruidas y selladas con el busybox recortado |
|
||
|
||
⚠ **Son 3 de las 24, no las 24.** Las otras 21 —entre ellas `linux`, `llama-cpp`, `protobuf`,
|
||
`qdrant`— no se probaron: son horas de cómputo en una caja que está con la campaña de KDE. Si alguna
|
||
usa un applet retirado, se verá en su próximo build y el vigía **no** lo habría anticipado, porque el
|
||
llamador estaría en un `configure` de terceros.
|
||
|
||
## El coste, dicho antes de que sorprenda
|
||
|
||
**Cambiar esta lista re-hashea busybox, y con él las 24 recetas que 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`. **`linux` está en la lista**: el kernel se reconstruye.
|
||
La granja lo hará en su propio ciclo; no se disparó a mano.
|
||
|
||
## El arranque — PROBADO en QEMU, contra una línea base
|
||
|
||
**2026-09-21.** Se ensamblaron **dos** Stage 1 completos y se arrancaron los dos en QEMU, para que
|
||
cualquier diferencia se pueda atribuir al recorte y no al azar:
|
||
|
||
| | rootfs sellado | applets en `/bin /sbin /usr/bin /usr/sbin` | qemu |
|
||
|---|---|---|---|
|
||
| **base** (busybox sin recortar) | `b3:1a3823b4…` | 400 | `rc=0` |
|
||
| **recortado** | `b3:3aa8cd2d…` | **278** | `rc=0` |
|
||
|
||
Los dos arrancan, los dos dan shell y los dos se apagan limpio. **Diff de las dos consolas: 35
|
||
líneas, y ninguna es funcional** — frecuencia del TSC, direcciones del RAMDISK, BogoMIPS, hora del
|
||
RTC. La única diferencia real es la que se buscaba:
|
||
|
||
```
|
||
~ # ls /bin /sbin /usr/bin /usr/sbin | wc -l
|
||
410 ← base
|
||
288 ← recortado
|
||
```
|
||
|
||
Criterios del runbook `stage1-vm-boot.md` §6, verificados en el recortado:
|
||
|
||
- **PID 1** — `arje-zero` incarna y corre.
|
||
- **Seed** — la card se carga: la prueba es que la `console-getty` llegó a incarnar.
|
||
- **getty → shell** — `/bin/busybox getty -n -l /bin/sh 115200 console` da prompt `~ #`, y
|
||
`echo MARCA-SHELL-VIVA` responde. **`getty` y `sh` sobrevivieron al recorte, que era el riesgo.**
|
||
- **`poweroff -f`** — `ACPI: PM: Preparing to enter system sleep state S5` / `reboot: Power down`.
|
||
Uno de los cinco indultados, probado **en vivo**: sin él la caja no se apaga.
|
||
|
||
⚠ **Cómo se corrió, porque condiciona el resultado:** en `dev.gioser.net`, con **QEMU en TCG
|
||
(emulación pura)** — el LXC no expone `/dev/kvm`, aunque el CPU sí trae los flags. Por eso el guion
|
||
se le manda por consola serie con esperas generosas. Hubo que instalar `qemu-system-x86-core` (dnf)
|
||
e ingerir de nuevo la semilla de Stage 0, que no estaba en el store del worker; sí salió con el
|
||
mismo hash que el manifiesto ya anotaba (`3ce721ec…`).
|
||
|
||
⚠ **Es Stage 1, no el producto.** Se eligió porque es donde vive busybox (`musl busybox hammerd
|
||
arje-zero`) y porque necesitaba 2 artefactos en vez de 8. `product-boot-test.sh` —que además levanta
|
||
sshd y netup— sigue sin correrse: le faltan `netup openssh uutils findutils-xargs diffutils
|
||
ripgrep` en el store del worker.
|
||
|
||
## Lo que queda del paso 1
|
||
- **Las otras 21 dependientes**, cuando la granja llegue.
|
||
- El siguiente corte natural son los `BORRAR` que no se tocaron por prudencia y los 272 que siguen:
|
||
el número baja a 0 por el paso 2 (declarar la munición sellada) y el 5 (`grep`/`sed` POSIX).
|