> 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…
hammer hash: dry-run que faltaba — cierra el sobre-reporte del static-audit de raíz
El agujero que dejé señalado 3 veces esta sesión: nada podía saber el sellado VIGENTE de una receta
sin construirla, así que static-audit.sh auditaba el más reciente por mtime (ls -dt) y acusaba a
recetas ya sanas (dbus/libnl) por un sellado anterior a sus flags.
FIX = subcomando `hammer hash <receta> [--check]`. Calcula el ArtifactHash puro sobre las recetas
(source_id + compiler/target/link + patches + flags + fases + hashes de deps recursivos) SIN bajar
fuentes ni compilar. artifact_hash() ya era pub; el CLI sólo lo expone. Cero cambios en la lógica
de hashing ⇒ NINGÚN sellado se mueve.
Verificado: `hash` da EXACTAMENTE el mismo hash que `build` (samurai, byte a byte); `--check` sobre
receta editada → NO-SELLADO en 2ms, exit 1, sin construir nada.
static-audit.sh ahora selecciona el artefacto VIGENTE por hash, no el más reciente por mtime. Con
fallback a ls -dt si hammer no está compilado. Efecto en el store completo: las ~65 recetas cuyo
sellado no es el vigente pasan de 'auditadas' (falsa cobertura) a 'sin artefacto' (deuda de rebuild
REAL, ahora visible): estáticas de verdad 615 | MIENTEN 0 | sin artefacto 123. Corre en 15s, sin
build. El '58 sin medir' de antes estaba enmascarando ~65 recetas más.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
diff --git a/crates/hammer-cli/src/main.rs b/crates/hammer-cli/src/main.rs
index 1f85091c..3a8c4977 100644
--- a/crates/hammer-cli/src/main.rs
+++ b/crates/hammer-cli/src/main.rs
@@ -37,6 +37,18 @@ enum Cmd {
/// Ruta a la receta TOML.
recipe: String,
},
+ /// [Fase 0] Calcula el `ArtifactHash` de una receta SIN construir — puro sobre las recetas
+ /// (source_id + compiler/target/link + patches + flags + fases + hashes de deps). No baja
+ /// fuentes ni compila. Responde "¿cuál es el hash VIGENTE de esta receta?", que el store no
+ /// puede dar sin sellar. Con `--check`: exit 0 si el artefacto vigente ya está en el store,
+ /// 1 si no — el dry-run que le faltaba a scripts/static-audit.sh para no auditar sellados rancios.
+ Hash {
+ /// Ruta a la receta TOML.
+ recipe: String,
+ /// No imprime el hash; verifica si el artefacto vigente está sellado en el store (exit 0/1).
+ #[arg(long)]
+ check: bool,
+ },
/// [Atestación] Verifica un árbol FHS (p.ej. un product-rootfs hidratado) contra su
/// `/ente/attest.json`: recomputa el BLAKE3 de cada binario crítico y lo compara. Es el gate de
/// integridad que arje aplicará al boot, hecho hoy por hammer ("reproducir, no confiar"). Exit≠0
@@ -755,6 +767,24 @@ fn main() -> anyhow::Result<()> {
let hash = hammer_build::build(&recipe, &cfg, &store)?;
println!("{hash}");
}
+ Cmd::Hash { recipe, check } => {
+ let store = hammer_core::Store::open(&cli.store)?;
+ let recipe = hammer_core::Recipe::load_from_path(&recipe)?;
+ let hash = hammer_build::artifact_hash(&recipe, &store)?;
+ if check {
+ // ¿el artefacto VIGENTE (el que corresponde a la receta de HOY) ya está sellado?
+ // Es lo que static-audit.sh no podía saber: `ls -dt store/*-<n>` da el más RECIENTE,
+ // no el vigente. Exit 0 = sellado al día; 1 = haría falta construir.
+ if store.has(&hash, &recipe.name) {
+ println!("{hash}\tSELLADO");
+ } else {
+ println!("{hash}\tNO-SELLADO");
+ std::process::exit(1);
+ }
+ } else {
+ println!("{hash}");
+ }
+ }
Cmd::Attest { rootfs } => {
let report = hammer_bootstrap::verify_attestation(std::path::Path::new(&rootfs))?;
for p in &report.ok {
diff --git a/scripts/static-audit.sh b/scripts/static-audit.sh
index d420297b..b6d9db02 100755
--- a/scripts/static-audit.sh
+++ b/scripts/static-audit.sh
@@ -37,6 +37,7 @@ set -eu
HUB="$(cd "$(dirname "$0")/.." && pwd)"
cd "$HUB"
STORE="${STORE:-$PWD/store}"
+HAMMER="${HAMMER:-$HUB/target/release/hammer}"
if [ $# -gt 0 ]; then
recetas=""
@@ -49,17 +50,27 @@ mienten=0; honestos=0; sin=0
for f in $recetas; do
n=$(basename "$f" .toml)
grep -qE '^[[:space:]]*link[[:space:]]*=[[:space:]]*"static"' "$f" || continue
- # `ls -dt`: el artefacto MÁS RECIENTE, no el alfabéticamente primero. El store guarda TODOS los
- # sellados de una receta (expat tenía 5, de junio a hoy) y el glob ordena por hash, que es
- # ruido: auditaba un artefacto de hace tres semanas y reportaba dinámica una receta ya
- # arreglada. Mismo bug de clase que comparar `libpng.a` con `libpng16.a` por `find | head -1`.
+ # EL ARTEFACTO VIGENTE, por hash — NO el más reciente por mtime. `hammer hash` calcula el
+ # ArtifactHash de la receta de HOY sin construir (~2ms, puro sobre las recetas). El store guarda
+ # TODOS los sellados históricos de una receta (expat tenía 5); auditar el más reciente por mtime
+ # sobre-reportaba (acusaba a dbus/libnl, ya sanas, por un sellado anterior a sus flags). Ahora
+ # auditamos EXACTAMENTE el sellado que corresponde a la receta actual, o ninguno.
#
- # HONESTIDAD: "más reciente" ≠ "vigente". Lo vigente sería el artefacto cuyo hash corresponde a
- # la receta de HOY, y hammer no expone ese hash sin construir (no hay `hammer hash`/dry-run).
- # Con el store al día ambos coinciden; si acabás de construir una receta vieja, no. Por eso el
- # audit se corre DESPUÉS del rebuild, nunca antes.
- art=$(ls -dt "$STORE"/*-"$n" 2>/dev/null | head -1) || true
- [ -z "${art:-}" ] && { sin=$((sin + 1)); continue; }
+ # Fallback a `ls -dt` si `hammer` no está compilado: el audit sigue corriendo, sólo vuelve a
+ # poder sobre-reportar (como antes de que existiera `hammer hash`).
+ if [ -x "$HAMMER" ]; then
+ h=$("$HAMMER" --store "$STORE" hash "$f" 2>/dev/null) || h=""
+ if [ -n "$h" ] && [ -d "$STORE/${h#b3:}-$n" ]; then
+ art="$STORE/${h#b3:}-$n"
+ else
+ # No hay sellado para la receta VIGENTE ⇒ no hay nada honesto que auditar (rancio no
+ # cuenta). Es el "sin artefacto" legítimo: la receta cambió y falta rebuild.
+ sin=$((sin + 1)); continue
+ fi
+ else
+ art=$(ls -dt "$STORE"/*-"$n" 2>/dev/null | head -1) || true
+ [ -z "${art:-}" ] && { sin=$((sin + 1)); continue; }
+ fi
# El primer ELF *ejecutable* del artefacto. Una `.so` tiene NEEDED por definición y no prueba
# nada; sólo el ejecutable dice si el enlace final fue estático.