SDD 30 §4b: la seed de producto ya sale de la RECETA — y openssh reproduce bit a bit
El emisor lee la receta que viaja DENTRO del artefacto sellado (el sidecar
.hammer/recipe.toml) y no de recipes/, porque seal_product_rootfs sirve a las
DOS vetas —product desde recetas locales y product_from_repo desde el repo
firmado— y sólo una tiene recipes/ a mano. El sidecar es la única fuente que
ambas comparten, que es lo que sostiene que converjan al mismo product-rootfs.
LA TRAMPA QUEDA ARMADA, NO CERRADA: el sidecar no entra al ArtifactHash, así que
un artefacto anterior al bloque [[service]] es un cache-hit válido que no se
reconstruye solo. Si el emisor devolviera lista vacía sin protestar, el producto
saldría booteando, verde y SIN SSHD. Falla ruidosamente, y hay un test que exige
que el mensaje siga explicando la causa Y dando el arreglo (regla 3 del
CLAUDE.md: un ausente falla ruidosamente, un vacío llega hasta el final diciendo
que todo fue bien).
LA MIGRACIÓN, CON CONTROL, y el hecho nuevo que dejó: openssh no estaba
certificado como reproducible ("el criterio es construye y corre", dice su
receta). Manifiesto de 33 sha256+modos ANTES de tocar nada, respaldo por
hardlink, borrado, rebuild bajo flock -o, comparación. Resultado: 33/33
ficheros, modos idénticos, y la ÚNICA diferencia en todo el árbol es
.hammer/recipe.toml. openssh REPRODUCE BIT A BIT.
Gotcha que costó el primer intento: store/ es bind-mount de /dev/sdb y work/
vive en /dev/sdc ⇒ `cp -al` cruza mounts y muere con EXDEV. El respaldo va
DENTRO del mount del store; un dot-dir ahí es invisible para store-gc.sh, que
sólo mira dirs ^[0-9a-f]{64}-.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# SDD 30 — Los servicios que trae un paquete
|
||||
|
||||
> **Estado:** §3 y §4a IMPLEMENTADOS (2026-09-12), §4b/§4c y §5 abiertos. Nace de una pregunta directa:
|
||||
> **Estado:** §3, §4a y §4b IMPLEMENTADOS (2026-09-12/13), §4c y §5 abiertos. Nace de una pregunta directa:
|
||||
> *«los paquetes que tienen sus servicios para systemd, openrc u otros, ¿ya saben empaquetarse en
|
||||
> takana con arje?»*. La respuesta medida era **no**, y este documento dice exactamente qué faltaba,
|
||||
> qué se cerró y qué queda.
|
||||
@@ -142,24 +142,45 @@ CONTROL, que tiene que pasar en verde**— incluidos «dos recetas del mismo per
|
||||
label» (ERROR) y «el mismo label en dos colas distintas» (NO es colisión: `upower` existe
|
||||
legítimamente en incoming-gnome y en incoming-kde, y son la misma unidad en dos imágenes).
|
||||
|
||||
**(b) El emisor, y la trampa del sidecar rancio.** `seal_product_rootfs()` sirve a las dos vetas
|
||||
(`product` desde recetas locales, `product_from_repo` desde el repo firmado) y **sólo una tiene el
|
||||
directorio de recetas**. La fuente uniforme que ambas comparten es el **sidecar de provenance**
|
||||
`.hammer/recipe.toml`, que `takana-build` congela DENTRO del artefacto y `Store::recipe_for_hash`
|
||||
sabe leer.
|
||||
**(b) El emisor — HECHO, y la trampa quedó armada en un test.** `service_cards()` lee la receta que
|
||||
viaja DENTRO de cada artefacto sellado (el sidecar de provenance `.hammer/recipe.toml`, vía
|
||||
`Store::recipe_for_hash`) y filtra `scope = "system"`. Se lee de ahí y no de `recipes/` porque
|
||||
`seal_product_rootfs()` sirve a las DOS vetas —`product` desde recetas locales y `product_from_repo`
|
||||
desde el repo firmado— y **sólo una tiene `recipes/` a mano**: el sidecar es la única fuente que
|
||||
ambas comparten, que es lo que sostiene que las dos converjan al mismo `product-rootfs`.
|
||||
|
||||
Pero ahí está la trampa, y es de la familia de «el cache-hit congela regresiones»: el sidecar
|
||||
**no entra en el `ArtifactHash`**, así que el openssh ya sellado en
|
||||
`store/938835e6…-openssh/.hammer/recipe.toml` **no tiene** el bloque `[[service]]` y, como el hash
|
||||
no se movió, `store.has()` da verdadero y **nunca se va a reconstruir solo**. Derivar la seed del
|
||||
sidecar hoy produciría un producto **sin sshd**, en silencio.
|
||||
La trampa era real y sigue ahí: **el sidecar no entra en el `ArtifactHash`**, así que un artefacto
|
||||
sellado antes de que su receta declarara servicios es un cache-hit perfectamente válido y no se
|
||||
reconstruye nunca solo. Si el emisor devolviera lista vacía sin protestar, el producto saldría
|
||||
**booteando, verde y sin sshd**. Por eso `service_cards()` **falla ruidosamente** con un mensaje que
|
||||
explica la causa y da el arreglo, y hay un test
|
||||
(`service_cards_falla_ruidosamente_con_sidecar_rancio`) que exige que el mensaje siga diciendo las
|
||||
dos cosas. Es la regla 3 del CLAUDE.md aplicada antes de pagarla: un ausente falla ruidosamente; un
|
||||
vacío llega hasta el final diciendo que todo fue bien.
|
||||
|
||||
⇒ La migración (re-sellar openssh para que su sidecar lleve el bloque) es una unidad de trabajo
|
||||
aparte y **con control**: hay que borrar el artefacto del store para forzar el rebuild, y openssh no
|
||||
está certificado como reproducible (su propia receta dice «el criterio es construye y corre»), así
|
||||
que el árbol nuevo hay que compararlo contra el viejo antes de darlo por bueno. Cambiar un artefacto
|
||||
que bootea en QEMU sin ese control es exactamente cómo se cuela una regresión que nadie ve.
|
||||
Hasta entonces `product_seed_card()` **sigue usando la constante**, a propósito y dicho acá.
|
||||
### 4b.1 La migración de openssh, medida — y un hecho nuevo
|
||||
|
||||
Re-sellar openssh era el paso que faltaba, y se hizo **con control** porque openssh no estaba
|
||||
certificado como reproducible (su propia receta dice «el criterio es construye y corre»). El
|
||||
procedimiento, por si hay que repetirlo con otro componente de servicio:
|
||||
|
||||
1. **Manifiesto de control ANTES de tocar nada**: `sha256` + modo de los 33 ficheros.
|
||||
2. **Respaldo por hardlink** — y acá saltó el gotcha conocido: `store/` es un bind-mount de
|
||||
`/dev/sdb` y `work/` vive en `/dev/sdc`, así que `cp -al` a `work/` muere con `Invalid
|
||||
cross-device link`. El respaldo tiene que ir **dentro del mount del store**; `store/.respaldo-…`
|
||||
sirve y es invisible para `store-gc.sh`, que sólo considera dirs `^[0-9a-f]{64}-`.
|
||||
3. Borrar el artefacto (los bytes sobreviven por el respaldo: 11 enlaces → 10) y reconstruir bajo
|
||||
`flock -o`.
|
||||
4. Comparar árbol nuevo contra el manifiesto.
|
||||
|
||||
**Resultado: openssh REPRODUCE BIT A BIT.** 33/33 ficheros, modos idénticos, y la única diferencia
|
||||
en todo el árbol es `.hammer/recipe.toml` — exactamente el sidecar que se quería migrar y nada más.
|
||||
Eso es un hecho nuevo sobre el corpus, no sólo un paso de esta campaña: openssh pasa de «no
|
||||
certificado» a «reproduce», comprobado contra un control tomado antes del borrado.
|
||||
|
||||
Coste que conviene saber: los árboles ya hidratados compartían inodes con el artefacto viejo, y el
|
||||
nuevo tiene inodes propios (`%h = 1`). El contenido es el mismo, pero hasta que esos árboles se
|
||||
refresquen el disco lleva dos copias.
|
||||
|
||||
**(c) Los de GNOME: DECLARADOS, no todavía arrancados por arje.** Las nueve unidades que
|
||||
`gnome-start-qemu.sh` levanta con `&` están declaradas en sus recetas y habilitadas en el perfil
|
||||
@@ -210,7 +231,8 @@ arriba en arje.
|
||||
| El vigía CORRE SOLO (cosecha-cron) → `docs/state/servicios.txt` | ✅ 0 errores, 18 avisos |
|
||||
| Los dos compat que no estaban en ningún perfil | ✅ corregido §4a |
|
||||
| Los 9 de GNOME declarados y habilitados (hash sin mover, 9/9) | ✅ §4c |
|
||||
| Emisor desde el sidecar + re-sellado de openssh | ◑ §4b (con control de reproducibilidad) |
|
||||
| Emisor desde el sidecar (`service_cards`, falla ruidosa + test) | ✅ §4b |
|
||||
| Re-sellado de openssh con control → **reproduce bit a bit** | ✅ §4b.1 |
|
||||
| Que la imagen los arranque por arje y no por el script `&` | ◻ §4c, depende de §4b |
|
||||
| `escritorio-cosmic`: auditar su `cosmic-start.sh` y habilitar | ◻ hoy sale con 5 AVISO |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user