diff --git a/docs/evidencia/brush-libtool-escapado-2026-09-21.diff b/docs/evidencia/brush-libtool-escapado-2026-09-21.diff new file mode 100644 index 00000000..e704f343 --- /dev/null +++ b/docs/evidencia/brush-libtool-escapado-2026-09-21.diff @@ -0,0 +1,98 @@ +# Evidencia del veredicto del paso 3 de docs/plan-botar-busybox.md (2026-09-21). +# libtool generado por el mismo configure de pkgconf, con dos shells distintos como /bin/sh. +# < = control, busybox 1.36.1 sh | > = brush 0.4.0 +# 25 líneas difieren, todas por un nivel de escapado de menos en el lado de brush. +# El fichero de la derecha ya no lo parsea NINGÚN shell (rc=2 en ambos, línea 368). + +62c62 +< ECHO="printf %s\\n" +--- +> ECHO="printf %s\n" +111c111 +< SP2NL="tr \\040 \\012" +--- +> SP2NL="tr \040 \012" +114c114 +< NL2SP="tr \\015\\012 \\040\\040" +--- +> NL2SP="tr \015\012 \040\040" +132c132 +< file_magic_cmd="\$MAGIC_CMD" +--- +> file_magic_cmd="$MAGIC_CMD" +144c144 +< sharedlib_from_linklib_cmd="printf %s\\n" +--- +> sharedlib_from_linklib_cmd="printf %s\n" +163c163 +< old_postinstall_cmds="chmod 644 \$oldlib~\$RANLIB \$tool_oldlib" +--- +> old_postinstall_cmds="chmod 644 $oldlib~$RANLIB $tool_oldlib" +176c176 +< global_symbol_pipe="/bin/sed -n -e 's/^.*[ ]\\([ABCDGIRSTW][ABCDGIRSTW]*\\)[ ][ ]*\\([_A-Za-z][_A-Za-z0-9]*\\)\$/\\1 \\2 \\2/p' | /bin/sed '/ __gnu_lto/d'" +--- +> global_symbol_pipe="/bin/sed -n -e 's/^.*[ ]\([ABCDGIRSTW][ABCDGIRSTW]*\)[ ][ ]*\([_A-Za-z][_A-Za-z0-9]*\)$/\1 \2 \2/p' | /bin/sed '/ __gnu_lto/d'" +179c179 +< global_symbol_to_cdecl="/bin/sed -n -e 's/^T .* \\(.*\\)\$/extern int \\1();/p' -e 's/^[ABCDGIRSTW][ABCDGIRSTW]* .* \\(.*\\)\$/extern char \\1;/p'" +--- +> global_symbol_to_cdecl="/bin/sed -n -e 's/^T .* \(.*\)$/extern int \1();/p' -e 's/^[ABCDGIRSTW][ABCDGIRSTW]* .* \(.*\)$/extern char \1;/p'" +185c185 +< global_symbol_to_c_name_address="/bin/sed -n -e 's/^: \\(.*\\) .*\$/ {\"\\1\", (void *) 0},/p' -e 's/^[ABCDGIRSTW][ABCDGIRSTW]* .* \\(.*\\)\$/ {\"\\1\", (void *) \\&\\1},/p'" +--- +> global_symbol_to_c_name_address="/bin/sed -n -e 's/^: \(.*\) .*$/ {"\1", (void *) 0},/p' -e 's/^[ABCDGIRSTW][ABCDGIRSTW]* .* \(.*\)$/ {"\1", (void *) \&\1},/p'" +188c188 +< global_symbol_to_c_name_address_lib_prefix="/bin/sed -n -e 's/^: \\(.*\\) .*\$/ {\"\\1\", (void *) 0},/p' -e 's/^[ABCDGIRSTW][ABCDGIRSTW]* .* \\(lib.*\\)\$/ {\"\\1\", (void *) \\&\\1},/p' -e 's/^[ABCDGIRSTW][ABCDGIRSTW]* .* \\(.*\\)\$/ {\"lib\\1\", (void *) \\&\\1},/p'" +--- +> global_symbol_to_c_name_address_lib_prefix="/bin/sed -n -e 's/^: \(.*\) .*$/ {"\1", (void *) 0},/p' -e 's/^[ABCDGIRSTW][ABCDGIRSTW]* .* \(lib.*\)$/ {"\1", (void *) \&\1},/p' -e 's/^[ABCDGIRSTW][ABCDGIRSTW]* .* \(.*\)$/ {"lib\1", (void *) \&\1},/p'" +261c261 +< libname_spec="lib\$name" +--- +> libname_spec="lib$name" +265c265 +< library_names_spec="\$libname\$release\$shared_ext\$versuffix \$libname\$release\$shared_ext\$major \$libname\$shared_ext" +--- +> library_names_spec="$libname$release$shared_ext$versuffix $libname$release$shared_ext$major $libname$shared_ext" +268c268 +< soname_spec="\$libname\$release\$shared_ext\$major" +--- +> soname_spec="$libname$release$shared_ext$major" +280c280 +< finish_cmds="PATH=\\\"\\\$PATH:/sbin\\\" ldconfig -n \$libdir" +--- +> finish_cmds="PATH="\$PATH:/sbin" ldconfig -n $libdir" +317c317 +< reload_cmds="\$LD\$reload_flag -o \$output\$reload_objs" +--- +> reload_cmds="$LD$reload_flag -o $output$reload_objs" +320c320 +< old_archive_cmds="\$AR \$AR_FLAGS \$oldlib\$oldobjs~\$RANLIB \$tool_oldlib" +--- +> old_archive_cmds="$AR $AR_FLAGS $oldlib$oldobjs~$RANLIB $tool_oldlib" +350c350 +< export_dynamic_flag_spec="\$wl--export-dynamic" +--- +> export_dynamic_flag_spec="$wl--export-dynamic" +353c353 +< whole_archive_flag_spec="\$wl--whole-archive\$convenience \$wl--no-whole-archive" +--- +> whole_archive_flag_spec="$wl--whole-archive$convenience $wl--no-whole-archive" +365,369c365,369 +< archive_cmds="\$CC -shared \$pic_flag \$libobjs \$deplibs \$compiler_flags \$wl-soname \$wl\$soname -o \$lib" +< archive_expsym_cmds="echo \\\"{ global:\\\" > \$output_objdir/\$libname.ver~ +< cat \$export_symbols | \$SED -e \\\"s/\\\\(.*\\\\)/\\\\1;/\\\" >> \$output_objdir/\$libname.ver~ +< echo \\\"local: *; };\\\" >> \$output_objdir/\$libname.ver~ +< \$CC -shared \$pic_flag \$libobjs \$deplibs \$compiler_flags \$wl-soname \$wl\$soname \$wl-version-script \$wl\$output_objdir/\$libname.ver -o \$lib" +--- +> archive_cmds="$CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname -o $lib" +> archive_expsym_cmds="echo "{ global:" > $output_objdir/$libname.ver~ +> cat $export_symbols | $SED -e "s/\(.*\)/\1;/" >> $output_objdir/$libname.ver~ +> echo "local: *; };" >> $output_objdir/$libname.ver~ +> $CC -shared $pic_flag $libobjs $deplibs $compiler_flags $wl-soname $wl$soname $wl-version-script $wl$output_objdir/$libname.ver -o $lib" +387c387 +< hardcode_libdir_flag_spec="\$wl-rpath \$wl\$libdir" +--- +> hardcode_libdir_flag_spec="$wl-rpath $wl$libdir" +426c426 +< export_symbols_cmds="\$NM \$libobjs \$convenience | \$global_symbol_pipe | \$SED 's/.* //' | sort | uniq > \$export_symbols" +--- +> export_symbols_cmds="$NM $libobjs $convenience | $global_symbol_pipe | $SED 's/.* //' | sort | uniq > $export_symbols" diff --git a/docs/plan-botar-busybox.md b/docs/plan-botar-busybox.md index cea9c13c..f3ec566f 100644 --- a/docs/plan-botar-busybox.md +++ b/docs/plan-botar-busybox.md @@ -141,8 +141,9 @@ frente C** y se pueden hacer hoy. 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`.** Correr con él los `configure` de tres recetas autotools del - corpus y el `product-boot-test.sh`. Terminado con un veredicto escrito: sirve, o falla en X. +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 @@ -174,3 +175,101 @@ aparecería en boot, en otra máquina, semanas después. 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 build recipes/.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 exacto no está determinado, y conviene decirlo así en vez de inventarlo.** Las tres +sospechas obvias se probaron aisladas y **las tres dan idéntico** en ambos shells: `echo` con barras +invertidas, sustitución de comando preservando `\\`, y el propio `func_quote_for_eval` de libtool +(`sed_quote_subst`) aplicado a mano. Lo medido es el diff de 25 líneas y que el `configure` termina +bien; el resto es hipótesis. + +## 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. **Hay un bug upstream que reportar** (reubeno/brush): `configure` de autotools genera un `libtool` + con un nivel de escapado de menos. El reproductor es una receta autotools cualquiera, y el + artefacto es el diff de 25 líneas de arriba. +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`. diff --git a/scripts/sh-banco-posix.sh b/scripts/sh-banco-posix.sh new file mode 100755 index 00000000..062dc6e0 --- /dev/null +++ b/scripts/sh-banco-posix.sh @@ -0,0 +1,86 @@ +#!/bin/sh +# Banco POSIX para candidatos a /bin/sh (SDD: docs/plan-botar-busybox.md §paso 3). +# Uso: scripts/sh-banco-posix.sh +# 57 casos POSIX ejecutados con `$SH -c`. Baseline medido 2026-09-21: busybox sh 56/57, +# brush 0.4.0 56/57 (diverge sólo en `shift` más allá de $#: devuelve 2, POSIX/busybox 1). +# NO es una prueba de suficiencia: el banco pasa y aun así brush no construye autotools. +SH="$1"; [ -n "$SH" ] || { echo "uso: $0 "; exit 2; } +ok=0; ko=0; KOLIST="" +t() { # t + nombre="$1"; esperado="$2"; codigo="$3" + got=$("$SH" -c "$codigo" 2>&1) + if [ "$got" = "$esperado" ]; then ok=$((ok+1)) + else ko=$((ko+1)); KOLIST="$KOLIST + ✗ $nombre + esperado: [$esperado] + obtenido: [$got]" + fi +} + +t "echo" "hola" 'echo hola' +t "pipeline" "2" 'printf "a\nb\n" | wc -l | tr -d " "' +t "redireccion" "x" 'echo x > /tmp/_p1 && cat /tmp/_p1' +t "aqui-doc" "linea" 'cat </dev/null; [ $? -ne 0 ] && echo 0' +t "expansion-largo" "3" 'v=abc; echo ${#v}' +t "quita-sufijo" "arch" 'v=arch.tar.gz; echo ${v%%.*}' +t "quita-prefijo" "gz" 'v=arch.tar.gz; echo ${v##*.}' +t "aritmetica" "7" 'echo $((3+4))' +t "aritmetica-var" "6" 'x=2; echo $((x*3))' +t "test-corchete" "si" '[ 1 -lt 2 ] && echo si' +t "test-cadena" "si" '[ "a" = "a" ] && [ -n "a" ] && echo si' +t "posicionales" "a:b:c" 'set -- a b c; IFS=:; echo "$*"' +t "todos-arroba" "a b c" 'set -- a b c; echo "$@"' +t "shift" "c" 'set -- a b c; shift 2; echo $1' +t "getopts" "v=1 f=x" 'set -- -v -f x; v=0; while getopts vf: o; do case $o in v) v=1;; f) f=$OPTARG;; esac; done; echo "v=$v f=$f"' +t "source-punto" "sourced" 'echo "echo sourced" > /tmp/_p2; . /tmp/_p2' +t "subshell-aisla" "fuera" 'x=fuera; ( x=dentro ); echo $x' +t "trap-exit" "adios" 'trap "echo adios" EXIT; true' +t "set-e-corta" "antes" 'set -e; echo antes; false; echo despues' +t "set-u" "0" 'set -u; ( echo $NODEF ) 2>/dev/null; [ $? -ne 0 ] && echo 0' +t "exec-redir" "z" 'exec 3>/tmp/_p3; echo z >&3; exec 3>&-; cat /tmp/_p3' +t "glob" "si" 'mkdir -p /tmp/_g && : > /tmp/_g/f1 && for f in /tmp/_g/f*; do [ -e "$f" ] && echo si; done' +t "glob-sin-match" "/tmp/_nada*" 'for f in /tmp/_nada*; do echo $f; done' +t "IFS-split" "a b c" 'IFS=,; set -- $(echo "a,b,c"); IFS=" "; echo "$*"' +t "comillas-preservan" "a b" 'x="a b"; echo "$x"' +t "sin-comillas-parte" "a b" 'x="a b"; echo $x' +t "escape-barra" 'a\b' 'printf "%s\n" "a\\b"' +t "printf-formato" "007" 'printf "%03d\n" 7' +t "read-var" "dato" 'echo dato | { read v; echo $v; }' +t "eval" "ev" 'c="echo ev"; eval $c' +t "dollar-dollar" "si" '[ -n "$$" ] && echo si' +t "PATH-busca" "si" 'command -v echo >/dev/null && echo si' +t "anidado-sust" "ab" 'echo "$(echo a)$(echo b)"' +t "multi-cmd-punto-coma" "a b" 'echo -n "a "; echo b' +t "negacion" "si" 'if ! false; then echo si; fi' +t "bucle-until" "3" 'i=0; until [ $i -ge 3 ]; do i=$((i+1)); done; echo $i' +t "break-continue" "1 3" 'r=; for i in 1 2 3 4; do [ $i = 2 ] && continue; [ $i = 4 ] && break; r="$r $i"; done; echo ${r# }' +t "return-funcion" "7" 'f() { return 7; }; f; echo $?' +t "here-string-noposix" "SKIP" 'echo SKIP' +t "wait-bg" "si" '(sleep 0.05) & wait; echo si' +t "unset-func" "si" 'f() { echo no; }; unset -f f; command -v f >/dev/null || echo si' +t "export-hijo" "val" 'export V=val; "'"$SH"'" -c "echo \$V"' +t "shift-sin-args" "1" 'set --; shift 2>/dev/null; echo $?' + +echo "$SH: OK=$ok FALLO=$ko" +[ -n "$KOLIST" ] && echo "$KOLIST" +exit $ko