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 Sutichaykuna

fix(build): la derivación de `work_root` hacía lo contrario de lo que su comentario prometía

El comentario de `defaults_for_store_with_lab` dice —y tiene razón— que `work_root` sigue al store
**porque el `seal` es un `rename`, atómico sólo dentro del mismo filesystem**, y que «ése es un
acoplamiento real, no una suposición».

⚠ **Pero `dirname(store)/work` sólo CONSIGUE eso si el store no es un punto de montaje.** Cuando lo
es, su padre está en otro filesystem y la derivación hace exactamente lo contrario de lo que
pretende, en silencio. Medido en el anfitrión hoy:

```
/store  → /dev/sdc   98 G      ⇒ dirname = "/"
/work   → /dev/sda4  68 G      ⇒ OTRO filesystem: el rename del seal NO es atómico
```

Y el precio no fue teórico: ese disco de 68 G **se llenó al 100 %** con 15 G de scratch que debía
estar en el de 98 G. El 21-sep alguien lo había parcheado con symlinks que se perdieron al rehacer
la jaula — y eso es la lección de fondo: **un arreglo que hay que recordar no es un arreglo.** Lo
mismo valdría para poner `TAKANA_WORK` en una shell o en un script: el día que alguien construya
desde otro sitio, vuelve el problema sin avisar.

⇒ Si el store es punto de montaje, el scratch va **dentro** del store, en `.work`: ahí está en su
filesystem POR CONSTRUCCIÓN, sin variable que nadie recuerde. El punto sigue la convención que el
store ya usa para lo que no es artefacto (`.dmerge`, `.delta-tree`, `.bootstrap-tmp`, `.apartado-*`).

La decisión va en una función PURA (`work_root_para`) y la consulta al filesystem en otra
(`store_es_punto_de_montaje`), que devuelve `false` cuando no puede mirar. Eso es lo conservador y
además deja las dos ramas testeables sin tocar disco: **sin evidencia de que la premisa esté rota,
no se cambia nada**.

Comprobado en las tres topologías:
- store normal (no montaje) ⇒ `/proj/work`, la derivación de siempre, intacta.
- store montado aparte ⇒ `.work` dentro. Contra el disco real: `/store es montaje=true ⇒
  work_root=/store/.work`.
- el WORKER no se mueve: `stat -c %d` da el mismo dev para `/opt/takana/store` y su padre, así que
  sigue en `/opt/takana/work` y no hay re-fetch de los árboles de fuentes.

12 tests del módulo en verde, incluidos los dos que ya cubrían la derivación.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

03e908fa20939db7003e6aeecc78d6d930971d21 · ruwaq Sergio · (6 p'unchaw ñawpaq) · tayta 99ddd2335 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 6

1 qillqakuna · +81 −5

diff --git a/crates/takana-build/src/config.rs b/crates/takana-build/src/config.rs
index f681d79b..63f120e8 100644
--- a/crates/takana-build/src/config.rs
+++ b/crates/takana-build/src/config.rs
@@ -80,18 +80,64 @@ impl BuildConfig {
     /// `work_root` SÍ sigue al store, y es deliberado: el `seal` final es un rename, que sólo es
     /// atómico dentro del mismo filesystem. Ése es un acoplamiento real, no una suposición.
     pub fn defaults_for_store_with_lab(store_root: &Path, lab: &Path) -> Self {
-        let store_parent = store_root
-            .parent()
-            .unwrap_or_else(|| Path::new("."))
-            .to_path_buf();
         Self {
             rootfs: lab.join("alpine"),
             zig_dir: lab.join("tools/zig"),
-            work_root: store_parent.join("work"),
+            work_root: work_root_para(store_root, store_es_punto_de_montaje(store_root)),
             cache_root: Some(lab.join("cache")),
         }
     }
 
+}
+
+/// ¿Es `store_root` un punto de montaje? (su `st_dev` difiere del de su padre)
+///
+/// Devuelve `false` cuando no se puede saber —alguna de las dos rutas no existe, como en los
+/// tests— y eso es lo conservador: sin evidencia de que la premisa esté rota, se mantiene la
+/// derivación de siempre.
+fn store_es_punto_de_montaje(store_root: &Path) -> bool {
+    use std::os::unix::fs::MetadataExt;
+    let Some(padre) = store_root.parent() else { return false };
+    let (Ok(a), Ok(b)) = (std::fs::metadata(store_root), std::fs::metadata(padre)) else {
+        return false;
+    };
+    a.dev() != b.dev()
+}
+
+/// Dónde va el scratch de build, dada la topología del store.
+///
+/// ══ POR QUÉ ESTO NO ES `dirname(store)/work` A SECAS ══════════════════════════════════════════
+/// El comentario de `defaults_for_store_with_lab` dice —y es correcto— que `work_root` sigue al
+/// store **porque el `seal` es un `rename`, atómico sólo dentro del mismo filesystem**. Eso es un
+/// acoplamiento real, no una suposición.
+///
+/// ⚠ Pero `dirname(store)/work` sólo CONSIGUE eso si el store **no** es un punto de montaje.
+/// Cuando lo es, su padre está en otro filesystem y la derivación hace exactamente lo contrario de
+/// lo que pretende, **en silencio**. Medido en el anfitrión el 2026-10-15:
+///
+///     /store  → /dev/sdc   (98 G)      ⇒ dirname = "/"
+///     /work   → /dev/sda4  (68 G)      ⇒ OTRO filesystem: el `rename` del seal NO es atómico
+///
+/// Y el precio no fue sólo teórico: ese disco de 68 G se llenó al 100 % con 15 G de scratch que
+/// debía estar en el de 98 G, y el 21-sep alguien lo había parcheado con symlinks que se perdieron
+/// al rehacer la jaula —un arreglo que hay que recordar no es un arreglo—.
+///
+/// ⇒ Si el store es un punto de montaje, el scratch va **dentro** del store, en `.work`: ahí está
+/// en su filesystem POR CONSTRUCCIÓN, sin variable de entorno que nadie recuerde. El punto va con
+/// la convención que el store ya usa para lo que no es un artefacto (`.dmerge`, `.delta-tree`,
+/// `.bootstrap-tmp`, `.apartado-*`).
+fn work_root_para(store_root: &Path, store_es_montaje: bool) -> PathBuf {
+    if store_es_montaje {
+        store_root.join(".work")
+    } else {
+        store_root
+            .parent()
+            .unwrap_or_else(|| Path::new("."))
+            .join("work")
+    }
+}
+
+impl BuildConfig {
     /// Lee overrides desde entorno (`TAKANA_LAB`, `TAKANA_ROOTFS`, `TAKANA_ZIG`, `TAKANA_WORK`,
     /// `TAKANA_CACHE`). `TAKANA_CACHE=""` (vacío) desactiva la caché.
     ///
@@ -151,6 +197,35 @@ mod tests {
         );
     }
 
+    /// El store NO es punto de montaje: la derivación de siempre, intacta.
+    #[test]
+    fn work_sigue_al_padre_cuando_el_store_no_es_montaje() {
+        assert_eq!(
+            work_root_para(Path::new("/proj/store"), false),
+            Path::new("/proj/work")
+        );
+    }
+
+    /// LA PREMISA QUE SE ROMPÍA EN SILENCIO: con el store montado aparte, `dirname(store)/work`
+    /// cae en OTRO filesystem y el `rename` del seal deja de ser atómico. Medido en el anfitrión:
+    /// `/store` en sdc y `/work` en sda4.
+    #[test]
+    fn work_entra_al_store_cuando_el_store_es_montaje() {
+        assert_eq!(
+            work_root_para(Path::new("/store"), true),
+            Path::new("/store/.work"),
+            "con el store montado aparte, el scratch va DENTRO: mismo FS por construcción"
+        );
+    }
+
+    /// Sin evidencia no se cambia nada: una ruta que no existe no prueba que la premisa esté rota.
+    #[test]
+    fn sin_poder_mirar_el_filesystem_se_mantiene_la_derivacion() {
+        assert!(!store_es_punto_de_montaje(Path::new(
+            "/no/existe/este/store/jamas"
+        )));
+    }
+
     #[test]
     fn find_lab_upward_encuentra_desde_un_subdirectorio() {
         let d = tempfile::tempdir().unwrap();
@@ -179,3 +254,4 @@ mod tests {
         assert_eq!(find_lab_upward(&raiz), None);
     }
 }
+