la imagen del servidor, armada y booteada — tres fallos que sólo aparecen ahí

Se armó la imagen del perfil `servidor` con gitea y se arrancó en QEMU. Los tres hallazgos, en el
orden en que aparecieron, son los que justifican probar la imagen en vez de dar por buena la receta.

1) ⚠⚠ ESCRIBIR EN EL ROOTFS HIDRATADO ES ESCRIBIR DENTRO DEL STORE

`takana users --merge` hacía `fs::write` sobre `<rootfs>/etc/passwd`. Un rootfs hidratado se arma con
HARDLINKS contra el store: medido, ese fichero y el del artefacto `product-rootfs` eran **el mismo
inode (1225824, 2 links, modo 444)**. Un `write` habría modificado el artefacto SELLADO, y todas las
imágenes futuras habrían salido con la cuenta metida dentro del producto. Acá se salvó porque el
store es de sólo lectura y salió `Permission denied` — confiar en eso es confiar en un permiso.

Ahora `escribir_rompiendo_hardlink()`: temporal + `rename`. Con su control, que afirma lo que
importa: tras escribir, el fichero del store **conserva su contenido**, baja a 1 link y el del
rootfs tiene otro inode. Verificado también sobre la imagen real.

2) LA IMAGEN TRAÍA EL BINARIO, LA CUENTA… Y NADIE LO ARRANCABA

Primera imagen: `gitea` instalado, `gitea:x:916` en `/etc/passwd`, y en el `genesis` de la seed sólo
`sshd`, `console-getty`, `hammerd`, `hammer-product`. `servidor-image.sh` no inyectaba las Cards —
eso sólo estaba en el camino de las imágenes de escritorio. Y ninguna métrica lo dice: `--services`
responde que el perfil lo habilita, y lo habilita; lo que faltaba era el paso que lleva esa
declaración a la imagen. Ahora llama a `service-cards` + `inyectar-cards.py` (idempotente por label).

3)  EL BINARIO MORÍA CON `trap invalid opcode` — Y EL BUG ESTABA EN EL BUILDER

Con la card en el genesis y la config puesta, gitea arrancaba y moría al instante:

    traps: gitea[91] trap invalid opcode ip:79ea992 ... in gitea[...]

El sandbox exporta `CC` apuntando a un wrapper que pone `-mcpu=baseline` (`sandbox.rs` ya avisaba:
«de paso cierra el SIGILL de AVX en qemu64»), y la fase Go del propio builder lo PISABA con
`CC="zig cc"` a secas — que por defecto es `-mcpu=native` y hornea la ISA del que compila. El binario
corría perfecto en el worker y moría en QEMU-TCG.

No falla al compilar ni al sellar: falla al EJECUTAR en otra CPU. Y el `ArtifactHash` no lo puede
cazar, porque la CPU del builder no entra en `hash_inputs` — dos workers distintos sellan bytes
distintos bajo la misma dirección. Arreglado en el builder y en la receta; re-hashea las CINCO
recetas `cgo = true` (gitea, usql, sq, gocryptfs, naabu), que es correcto: lo que había sellado no
es portable. gitea: `b3:391a613e…` → `b3:5edf9c16…`.

Lo verificado en la VM: PID 1 = arje-zero · la cuenta de `[[user]]` en `/etc/passwd` y `/etc/group`
de la imagen · la card de gitea en el `genesis` · y arje encarnándola, con la guarda saliendo 78 y
`/var/log/arje/ente-gitea.log` diciendo exactamente «falta /etc/gitea/app.ini — es config del SITIO».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
This commit is contained in:
Sergio
2026-09-14 16:43:29 +00:00
co-authored by Claude Opus 5
parent 44b3624de6
commit 467a87cdb8
5 changed files with 96 additions and 3 deletions
+20
View File
@@ -99,6 +99,26 @@ else
echo "==> cuentas : ninguna receta del perfil declara [[user]]"
fi
# ── 2 ter. las CARDS de los servicios que el perfil arranca (SDD 30 §4c) ─────────────────────────
# Sin esto la imagen trae el binario, trae la cuenta… y NADIE lo arranca: la seed del producto sólo
# conoce las cards que `takana-bootstrap` hornea (`sshd`, `console-getty`, `hammerd`). Medido en la
# primera imagen de este perfil: `gitea` instalado, su usuario en `/etc/passwd`, y en el `genesis`
# NADA — un servidor que arranca perfecto sin el servicio por el que existe. La métrica no lo ve:
# `--services` dice que el perfil lo habilita, y lo habilita; lo que faltaba era el paso que lleva
# esa declaración a la imagen.
#
# `service-cards` es la ÚNICA traducción `[[service]]` → Card, y `inyectar-cards.py` sólo compone la
# seed (idempotente por label, y con `os.replace` porque la seed es un hardlink al store).
SVCPATHS=$(python3 scripts/targets.py --service-paths "$PERFIL")
if [ -n "$SVCPATHS" ]; then
# shellcheck disable=SC2086
"$TAKANA" --store "$STORE" service-cards $SVCPATHS > "$WORKDIR/cards.json"
echo "==> cards : $(python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1]))))' "$WORKDIR/cards.json") card(s) de $(echo "$SVCPATHS" | wc -l) receta(s)"
python3 scripts/gnome/inyectar-cards.py "$RFS/ente/seed.card.json" "$WORKDIR/cards.json"
else
echo "==> cards : el perfil no arranca ningún servicio de paquete"
fi
# ── 3. reparar el init que la hidratación pisó (ver cabecera, punto 2) ───────────────────────────
antes=$(readlink "$RFS/sbin/init" 2>/dev/null || echo "(fichero)")
ln -sf /usr/bin/arje-zero "$RFS/sbin/init"