# simi — el `/bin/sh` del producto. Código takana-PROPIO, no una adopción. # # Mismo patrón de receta que netup/hammerd/arje-zero: construye un crate del workspace a un commit # fijado, deps vendoreadas para build hermético `--offline`, link estático con zig cc. Sólo depende # de libc ⇒ vendor mínimo. # # Por qué propio y no adoptado (ver `docs/plan-botar-busybox.md`): el único candidato Rust maduro es # brush, y se midió — 6,9 MB contra 1,17 del busybox entero, 4,1× la latencia de arranque, y dos # divergencias POSIX medidas (reubeno/brush#1394 y #1396, las dos reportadas por nosotros). simi son # 4448 líneas con CERO divergencias contra el ash de busybox en los 159 casos del banco diferencial. # # ══ MEDICIÓN VIGENTE — 2026-10-22, contra `b3:53f26620…` ══════════════════════════════════════ # ⚠ Esta nota citaba `b3:46ff6fcf…` y «159 casos», y las dos cifras habían CADUCADO: la receta # avanzó (hash vigente `b3:327b31b72d8051e0d86693d16090fb393b774622d120c5c140ad8673986fcc2d`) y el # banco creció. Re-medido contra el artefacto vigente, traído del worker al hub: # # · ELF x86-64 **estático** de verdad: `readelf -d` ⇒ **0 NEEDED** # · **529 808 bytes** stripped. La progresión del día, para que se vea qué cuesta cada cosa: # 498 736 (base) → 508 992 (+10 KB, las cuatro expansiones no-POSIX) → 529 808 (+21 KB, el # editor de línea con historial y Tab). Sigue siendo **la mitad** del busybox entero (0,92 MB) # y una fracción de brush (6,9 MB). # · banco diferencial vs el ash de busybox: **1339 casos** —1329 de ellos **fases REALES de # recetas del corpus**, no escritas a mano— ⇒ **DIVERGE=0, avisos(stderr)=0**, 1,5 ms/caso # contra los 1,5 del ash. (bash: 8 divergencias; brush: 35) # · latencia de arranque medida EN EL HUB contra el artefacto sellado: **464 µs contra 449 del # ash — 1,03×**. (Una corrida anterior dio 1,18×: la cifra es ruidosa, el propio banco avisa de # que incluye el fork+exec de Python y sirve para COMPARAR, no como absoluto.) brush: 3,5×. # # ⚠ **Y desde el 2026-10-22 es TAMBIÉN un shell interactivo** (`src/linea.rs`, a pedido del # usuario): historial en memoria y en `~/.simi_historial`, Tab que completa órdenes (builtins + # PATH) o rutas según la posición, flechas, Inicio/Fin/Supr, los controles de estilo emacs, y `PS1` # con `\u \h \w \$`. Escrito con termios + ANSI y sólo `libc`, por lo mismo que el resto: cada dep # de este binario entra en la raíz de confianza del arranque. # · **13/13** pruebas con un PTY real, y la TERMINAL queda idéntica al salir (`lflag` byte a # byte) — el `Drop` del modo crudo es lo que evita dejar la consola muda tras un panic # · lo que NO cubre, dicho en vez de fingido: control de trabajos, `Ctrl-R`, y líneas más largas # que el ancho de la terminal se redibujan feas (el contenido es correcto) # # ⚠ **Y cuatro expansiones de BASH**, a pedido explícito del # usuario: `${x/pat/rep}` `${x//pat/rep}` (con anclajes `#`/`%`), `${x:desde:largo}` y # `${x^} ${x^^} ${x,} ${x,,}`. Son añadidos PUROS —POSIX deja esa sintaxis sin definir— y está # verificado con control, no supuesto: el banco POSIX da los **MISMOS 2 fallos** preexistentes # (`while-read`, `getopts`) con el binario viejo y con el nuevo, y el diferencial sigue en # DIVERGE=0. `${!x}` NO se añadió: choca con el `$!` del lexer y era la menos usada. # # (Antes de eso, `wasi-libcxx` usaba `${t/unknown-/}` y era la ÚNICA divergencia de las 1339. Se # pasó la receta a POSIX igual, por higiene: una receta no debería depender de una extensión para # algo que POSIX expresa.) # # ⚠ Todavía NO está en ningún perfil de `docs/state/targets.toml`, y es a propósito: declararla es # meterla en la raíz de confianza del arranque (frente C) y eso es su propia unidad de trabajo. name = "simi" version = "0.0.1" license = "MIT" [source] # El propio takana. El commit es el identificador inmutable; subirlo cuando simi avance. repo = "https://git.gioser.net/sergio/takana.git" commit = "4bc9be4d066f48c9eaa70736d1f18e01f43da87a" [build] compiler = "zig-cc" target = "x86_64-linux-musl" link = "static" # Split de la info de depuración, igual que hammerd. Entra en `hash_inputs`. strip_debug = true # Sólo el binario `simi` del workspace. flags = ["-p", "simi"] # ⚠ La receta no instalaba `/bin/sh`, y la consecuencia era GRANDE y silenciosa: medido el # 2026-10-23 sobre el cierre de IMAGEN de `cli` (154 nodos), NINGÚN paquete provee `sh`. # # bin/bash ← bash # usr/bin/awk ← akllay # usr/bin/doas ← doas # bin/login ← shadow # sbin/agetty ← util-linux # bin/sh ← NADIE # # O sea que en una imagen hidratada todo script con `#!/bin/sh` falla, y el login de consola # —que es `agetty -l /bin/sh`— no tiene qué ejecutar. El rootfs de ARRANQUE sí lo tiene (el # bootstrap crea `bin/sh → /usr/bin/simi` desde que se quitó busybox), pero la imagen de un # perfil la arma `hydrate-profile.py`, que proyecta SÓLO el cierre del perfil y no parte del # rootfs de arranque. Dos fuentes de verdad que discrepaban, que es exactamente el modo de fallo # que la cabecera de ese script describe. # # El enlace lo pone el PROVEEDOR, no el hidratador: quien da la capacidad da su nombre canónico. # Es lo mismo que hace `os-release` con `/usr/lib/os-release`. [build.phases] install = ''' set -eu mkdir -p /out/usr/bin /out/bin # Replica lo que la fase derivada de Cargo hace —copiar los ejecutables de `target/*/release`— # porque declarar `install` la REEMPLAZA, no la amplía. Se prueban las dos rutas (nativa del # sandbox y por triple) en vez de hornear una: cuál aplica depende de `build.target`. for rel in target/release target/*/release; do [ -d "$rel" ] || continue find "$rel" -maxdepth 1 -type f -perm -100 -exec cp {} /out/usr/bin/ \; done ln -sfn /usr/bin/simi /out/bin/sh # ── guardián ───────────────────────────────────────────────────────────────────────────────── [ -x /out/usr/bin/simi ] || { echo "simi: no quedó el ejecutable — NO sello" >&2; exit 1; } # `-L` y no `-e`: el enlace es ABSOLUTO y dentro del artefacto apunta fuera, así que `-e` lo # sigue hasta un sitio que no existe y dice AUSENTE sobre un enlace correcto. Ya me mordió. [ -L /out/bin/sh ] || { echo "simi: falta /bin/sh — la imagen quedaría sin shell; NO sello" >&2; exit 1; } [ "$(readlink /out/bin/sh)" = /usr/bin/simi ] || { echo "simi: /bin/sh apunta a $(readlink /out/bin/sh), no a /usr/bin/simi — NO sello" >&2; exit 1; } # Y que CORRA, con las tres capacidades que el commit pineado tiene que traer. Un binario que # sella y no corre ya pasó en este corpus más de una vez. /out/usr/bin/simi -c 'alias x=echo; x alias-ok' | grep -q alias-ok || { echo "simi: el binario no trae alias — el commit pineado es viejo; NO sello" >&2; exit 1; } /out/usr/bin/simi -c 'set -- -a; while getopts a o; do echo "ok=$o"; done' | grep -q 'ok=a' || { echo "simi: el binario no trae getopts — el commit pineado es viejo; NO sello" >&2; exit 1; } /out/usr/bin/simi -c 'v="|a"; [ "${v#|}" = a ]' || { echo "simi: el operador # con patron | sigue roto — el commit pineado es viejo; NO sello" >&2; exit 1; } echo "simi: $(stat -c%s /out/usr/bin/simi) B · /bin/sh -> $(readlink /out/bin/sh) · alias+getopts OK" ''' # Sin `[build.phases]`: igual que hammerd y netup, el BuildSys::Cargo del lab instala el binario del # crate. **El symlink `sh` NO lo pone esta receta**, y eso es a propósito: quién gana como `/bin/sh` # lo decide el ensamblado del producto (`USERLAND_COMPONENTS` en `crates/takana-bootstrap/src/lib.rs`, # que hidrata el userland Rust DESPUÉS de busybox para que ensombrezca sus applets, y `lib.rs:676`, # que retira el homónimo de los otros directorios). Ese cableado es frente C y es su propia unidad de # trabajo: mete a simi en la raíz de confianza del arranque, y eso no se hace de paso.