> 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…
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>
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);
}
}
+