tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Definitions

aichat: era NO REPRODUCIBLE, y la causa estaba en el `build.rs` de un crate transitivo

`scripts/verificar-repro.sh` lo cazó en el worker: dos reconstrucciones seguidas, misma máquina y
mismo lab, dan `usr/bin/aichat` distintos. Es el primer no-determinismo REAL que el instrumento
encuentra —hasta hoy todo lo que había marcado era deriva— y salió porque el arreglo de esta tarde
conserva los dos ejemplares en `store/.divergen/` en vez de borrarlos.

`hammer why-differs` dijo `.rodata`. El primer byte que cambia (offset 1749687) cae dentro de un JSON
de *stopwords* por idioma embebido en el binario, y lo que difiere no es el contenido sino el ORDEN:
una build empieza por `"sl"` y la otra por `"fi"`.

De ahí a la causa: el crate es `stop-words` 0.8.1, transitivo vía bm25, y su `build.rs` arma un
`HashMap<String, Vec<String>>` y lo serializa con `serde_json::to_string`. El `HashMap` de Rust usa
`RandomState` —semilla ALEATORIA POR PROCESO— así que el orden de serialización cambia en cada
compilación y ese JSON se hornea en el binario. `BTreeMap` serializa en orden de clave. Es un arreglo
de una palabra que upstream no hizo.

Se parchea el crate vendorizado en una fase `configure` (el vendoreo ocurre ANTES de las fases, así
que el árbol ya está ahí) y se vacía la lista de ficheros de su `.cargo-checksum.json`, por la misma
razón y con la misma técnica que `recipes/firefox.toml`: cargo verifica el sha de cada fichero
vendorizado y no ofrece forma de recalcularlo.

Los `[ -f ]` fallan ruidoso a propósito. Si mañana la cadena de deps deja de traer stop-words, lo
peor sería que el parche se saltara en silencio y el binario volviera a ser aleatorio sin que nadie
se entere — que es exactamente cómo esto llegó hasta acá.

Re-hashea aichat (b3:c40a15bd → b3:7a515b0a), y eso es gratis: `yupana radio` da 0 dependientes y
ninguna imagen lo declara.

e348c97964de8e5232b84173fd9a018541326701 · by Sergio · (1 month ago) · parent a2e5ebd6d · browse the tree at this commit

Semantic delta

No definition changed: this change is text, documentation or data.

1 files · +29 −0

diff --git a/recipes/aichat.toml b/recipes/aichat.toml
index 00017165..5da12e2b 100644
--- a/recipes/aichat.toml
+++ b/recipes/aichat.toml
@@ -17,4 +17,33 @@ link     = "static"
 flags    = ["--bin", "aichat"]
 
 [build.phases]
+# ── ⚠ SIN ESTO, aichat NO ES REPRODUCIBLE ──────────────────────────────────────────────────────
+# Medido el 2026-09-05 con `scripts/verificar-repro.sh`: dos reconstrucciones seguidas, misma
+# máquina y mismo lab, dan `usr/bin/aichat` distintos en `.rodata`. El primer byte que cambia cae
+# dentro de un JSON de *stopwords* por idioma embebido en el binario, y lo que difiere es el ORDEN
+# de los idiomas — una build empieza por `"sl"`, la otra por `"fi"`.
+#
+# La causa está en el `build.rs` del crate `stop-words` 0.8.1 (transitivo, vía bm25): arma un
+# `HashMap<String, Vec<String>>` y lo serializa con `serde_json::to_string`. El `HashMap` de Rust
+# usa `RandomState`, o sea una semilla ALEATORIA POR PROCESO ⇒ el orden de serialización cambia en
+# cada compilación, y ese JSON se hornea en el binario. `BTreeMap` serializa en orden de clave y es
+# determinista; es un arreglo de una palabra que upstream no hizo.
+#
+# El `.cargo-checksum.json` se vacía por la misma razón que en `recipes/firefox.toml`: cargo
+# verifica el sha de CADA fichero vendorizado y no ofrece forma de recalcularlo, así que se deja el
+# checksum del paquete y se vacía la lista de ficheros (lo mismo hace el APKBUILD de Alpine).
+#
+# Los `[ -f ]` fallan RUIDOSO a propósito: si mañana la cadena de deps deja de traer stop-words, lo
+# que no queremos es que el parche se salte en silencio y el binario vuelva a ser aleatorio sin que
+# nadie se entere — que es exactamente cómo esto llegó hasta acá.
+configure = '''
+set -e
+f=vendor/stop-words/build.rs
+[ -f "$f" ] || { echo "FALTA $f — ¿cambió la cadena de deps? Ver la nota de arriba." >&2; exit 1; }
+sed -i 's/HashMap/BTreeMap/g' "$f"
+grep -q 'BTreeMap<String, Vec<String>>' "$f" || { echo "el sed no agarró en $f" >&2; exit 1; }
+c=vendor/stop-words/.cargo-checksum.json
+[ -f "$c" ] || { echo "FALTA $c" >&2; exit 1; }
+sed -i 's/\("files":{\)[^}]*/\1/' "$c"
+'''
 install = "mkdir -p /out/usr/bin && cp target/release/aichat /out/usr/bin/aichat"