`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.
50 lines
2.7 KiB
TOML
50 lines
2.7 KiB
TOML
# Importada de nixpkgs por `hammer import-nix` (Etapa G). PUNTO DE PARTIDA, no final:
|
|
# - el build usa el lab de hammer (zig-cc / musl estático), NO el stdenv de nix ⇒ revisá
|
|
# compiler/link/phases y adaptá hasta que compile.
|
|
# - las deps van con su nombre NIX; remapealas a las recetas del corpus si difieren.
|
|
name = "aichat"
|
|
version = "0.30.0"
|
|
license = "MIT OR Apache-2.0"
|
|
|
|
[source]
|
|
repo = "https://github.com/sigoden/aichat"
|
|
commit = "430416d914896c3534c04b84c0226910c64e3e66"
|
|
|
|
[build]
|
|
compiler = "zig-cc"
|
|
target = "x86_64-linux-musl"
|
|
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"
|