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 1430Shell 145Markdown 87Rust 852973 Sutichaykuna

store: un destino VACÍO ya no se traga el sellado en silencio

`has()` define presencia como «tiene al menos una entrada» —hay test desde hace tiempo—
pero `seal()` usaba `is_dir()`, que también es cierto para un directorio vacío. Con eso
`seal` devolvía Ok, tiraba el árbol recién construido, y el build logueaba `sealed` y
salía 0. Dos nociones distintas de «está» en el mismo struct, y la que decide si el
trabajo se guarda era la mala.

Medido en el worker dev.gioser.net: 1457 directorios vacíos en el store se comían cada
sellado. `openssl-threads` compilaba entero —headers, libcrypto.a, libssl.a, los .pc— y
sellaba NADA; python3 se construía después sin openssl y quedaba sin `_ssl`, y con eso
morían glib y 4 recetas más de GNOME. El error salía a tres recetas de distancia de la
causa y no nombraba openssl ni una vez. Probado quitando el vacío: el MISMO build sella
150 ficheros con OPENSSL_THREADS, mismo hash.

Se usa `remove_dir` y no `remove_dir_all`: sólo tiene éxito sobre un vacío, así que si
algo dejó contenido ahí entremedio falla ruidoso en vez de borrar un artefacto bueno.

No mueve hashes (testigo zlib idéntico).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4

1eb39976b089ba7e46a1ebc40959b798a62b07d9 · ruwaq Sergio · (1 killa ñawpaq) · tayta b3cc18018 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 2

crates/hammer-core/src/store.rs

1 qillqakuna · +53 −1

diff --git a/crates/hammer-core/src/store.rs b/crates/hammer-core/src/store.rs
index 278784f3..6a890225 100644
--- a/crates/hammer-core/src/store.rs
+++ b/crates/hammer-core/src/store.rs
@@ -136,7 +136,34 @@ impl Store {
     ) -> crate::Result<PathBuf> {
         let dst = self.path_of(h, name);
         if dst.is_dir() {
-            return Ok(dst);
+            // ── UN DIRECTORIO VACÍO NO ES UN ARTEFACTO, ES UN NOMBRE ──────────────────────────
+            // `has()` (arriba) ya define presencia como "tiene AL MENOS una entrada". Acá se usaba
+            // `is_dir()`, que también es verdadero para un directorio VACÍO ⇒ `seal` devolvía Ok,
+            // TIRABA el árbol recién construido y el build logueaba `sealed` y salía 0. Dos
+            // nociones distintas de "está" en el mismo struct, y la de `seal` es la que decide si
+            // el trabajo se guarda.
+            //
+            // Medido en el worker dev.gioser.net (2026-08-29): 1457 directorios vacíos en el store
+            // se comían cada sellado. `openssl-threads` compilaba entero —instalaba headers,
+            // libcrypto.a, libssl.a, los .pc— y sellaba NADA; después python3 se construía sin
+            // openssl y quedaba sin `_ssl`, y con eso morían glib y 4 recetas más de GNOME. El
+            // error salía a 3 recetas de distancia de la causa y no mencionaba openssl ni una vez.
+            //
+            // `remove_dir` (NO `remove_dir_all`) a propósito: sólo tiene éxito sobre un directorio
+            // vacío. Si algo dejó contenido ahí entremedio, esto falla RUIDOSO en vez de borrar un
+            // artefacto bueno — que es la regla: un ausente falla ruidosamente, un vacío no.
+            let vacio = std::fs::read_dir(&dst)
+                .map(|mut e| e.next().is_none())
+                .unwrap_or(false);
+            if !vacio {
+                return Ok(dst);
+            }
+            std::fs::remove_dir(&dst).map_err(|e| {
+                crate::Error::Store(format!(
+                    "destino vacío {} no se pudo retirar antes de sellar: {e}",
+                    dst.display()
+                ))
+            })?;
         }
         if let Some(parent) = dst.parent() {
             std::fs::create_dir_all(parent)?;
@@ -417,4 +444,29 @@ sha256  = "abc123"
         assert_eq!(p1, p2);
         assert!(second.path().is_dir()); // intacto
     }
+
+    /// REGRESIÓN (2026-08-29): un `dst` VACÍO se tragaba el sellado en silencio.
+    /// `seal` usaba `is_dir()`, verdadero para un directorio vacío ⇒ devolvía Ok sin escribir
+    /// nada y el build decía `sealed`. En el worker eso dejó 1457 artefactos hueco y rompió
+    /// GNOME por una cadena de 3 recetas (openssl-threads vacío → python3 sin _ssl → glib).
+    #[test]
+    fn seal_no_se_traga_un_destino_vacio() {
+        let store_dir = tempfile::tempdir().unwrap();
+        let store = Store::open(store_dir.path()).unwrap();
+
+        // Un directorio vacío plantado en el store, como el que deja un rsync a medias.
+        let dst = store.path_of(&hash(), "demo");
+        std::fs::create_dir_all(&dst).unwrap();
+        assert!(dst.is_dir());
+        assert!(!store.has(&hash(), "demo")); // `has` ya lo rechazaba…
+
+        let out = tempfile::tempdir_in(store_dir.path()).unwrap();
+        std::fs::write(out.path().join("a"), b"contenido").unwrap();
+        let sellado = store.seal(&out.keep(), &hash(), "demo").unwrap();
+
+        // …y ahora `seal` tampoco lo acepta: el contenido llega al store de verdad.
+        assert_eq!(sellado, dst);
+        assert_eq!(std::fs::read(dst.join("a")).unwrap(), b"contenido");
+        assert!(store.has(&hash(), "demo"));
+    }
 }