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 1569Shell 203Rust 109Markdown 993728 Definiciones

imagen↔repo: install hidrata userland del repo firmado + 2 fixes de path

Cierra el lazo Etapa F+G: el userland del producto se arma vía `hammer install
--repo --prefix --require-signed` desde el repo firmado, no del bootstrap
hardcodeado. scripts/product-userland-from-repo.sh hidrata un set curado (13+
tools validados estáticos del repo).

Dos bugs reales del path install/pack encontrados+arreglados:
1. pack derivaba target_bin=/usr/bin/{name}; para paquetes con binario≠nombre
   (ripgrep→rg, repgrep→rgr) quedaba mal y el sanity-check de install fallaba.
   Ahora pack lo deriva del flag `--bin <X>` de la receta Cargo.
2. La reproducción del source_patch derivaba el NOMBRE de la receta del
   target_bin ⇒ "rg" ≠ "ripgrep" del corpus ⇒ no cache-hit ⇒ rebuild + dup en
   el store (find_by_hash ambiguo). build_source_patch/run_apply ahora reciben
   el nombre del paquete (install/bootstrap lo pasan; apply=None) ⇒ cache-hit
   del artefacto del corpus, sin duplicar.

(El "EACCES" inicial era sólo --store ausente: DEFAULT_STORE=/store root-only.)
hammer-build 49/49 tests ok; ripgrep install validado e2e (rg hidratado).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

d41f1705e697037a693430559816e85455d19965 · por sergio · (hace 3 meses) · padre 2207f1858 · ver el árbol en este commit

Delta semántico · 9

crates/hammer-bootstrap/src/lib.rs

crates/hammer-build/src/swm_bridge.rs

crates/hammer-cli/src/main.rs

crates/hammerd/src/bus.rs

5 archivos · +82 −9

diff --git a/crates/hammer-bootstrap/src/lib.rs b/crates/hammer-bootstrap/src/lib.rs
index 175620d6..318b6be9 100644
--- a/crates/hammer-bootstrap/src/lib.rs
+++ b/crates/hammer-bootstrap/src/lib.rs
@@ -829,7 +829,7 @@ pub fn install_components_from_repo(
         let mutation = source_patch_of(&swm).ok_or_else(|| {
             Error::Other(format!("'{name}' no es un paquete source_patch"))
         })?;
-        let hash = hammer_build::swm_bridge::build_source_patch(mutation, cfg, store, None)?;
+        let hash = hammer_build::swm_bridge::build_source_patch(mutation, cfg, store, None, Some(name))?;
         tracing::info!(%name, %hash, "product: componente reproducido desde el repo firmado");
         out.push((name.to_string(), hash));
     }
diff --git a/crates/hammer-build/src/swm_bridge.rs b/crates/hammer-build/src/swm_bridge.rs
index 302bd797..dc77ce38 100644
--- a/crates/hammer-build/src/swm_bridge.rs
+++ b/crates/hammer-build/src/swm_bridge.rs
@@ -26,9 +26,14 @@ pub fn build_source_patch(
     cfg: &BuildConfig,
     store: &Store,
     scratch_root: Option<&Path>,
+    name: Option<&str>,
 ) -> hammer_core::Result<ArtifactHash> {
     let catalog_dir = catalog_dir_for(cfg, scratch_root);
-    let recipe = recipe_from_source_patch(mutation, None, &catalog_dir)?;
+    // El NOMBRE de la receta sintetizada decide el sufijo del store (`<hash>-<name>`) Y la
+    // clave de cache-hit (`store.has(hash, name)`). Si quien llama conoce el nombre del paquete
+    // (p.ej. `install ripgrep`), lo pasa: así reproduce como "ripgrep" y cache-hitea el artefacto
+    // del corpus, en vez de derivar "rg" del target_bin y sellar un duplicado bajo otro nombre.
+    let recipe = recipe_from_source_patch(mutation, name, &catalog_dir)?;
 
     let expected = match mutation {
         Mutation::SourcePatch { expected_hash, .. } => expected_hash.clone(),
@@ -358,7 +363,7 @@ mod tests {
         let d = tempfile::tempdir().unwrap();
         let store = Store::open(d.path().join("store")).unwrap();
         let cfg = BuildConfig::defaults_for_store(store.root());
-        let err = build_source_patch(&m, &cfg, &store, None).unwrap_err().to_string();
+        let err = build_source_patch(&m, &cfg, &store, None, None).unwrap_err().to_string();
         assert!(err.contains("source_patch"), "{err}");
     }
 
@@ -388,7 +393,7 @@ mod tests {
         let cfg = BuildConfig::defaults_for_store(store.root());
         let scratch = d.path().join("scratch");
         // Falla (no hay rootfs), pero el patch ya debe estar descargado.
-        let _ = build_source_patch(&m, &cfg, &store, Some(&scratch));
+        let _ = build_source_patch(&m, &cfg, &store, Some(&scratch), None);
         // El patch se materializa como `{label}-{source_key}.patch` (label = derive_name del
         // target_bin) para no colisionar entre deps en el catálogo.
         let landed = scratch.join("swm-recipes").join(format!("x-{commit}.patch"));
diff --git a/crates/hammer-cli/src/main.rs b/crates/hammer-cli/src/main.rs
index 310ac9d1..4d8af8e9 100644
--- a/crates/hammer-cli/src/main.rs
+++ b/crates/hammer-cli/src/main.rs
@@ -795,6 +795,7 @@ fn main() -> anyhow::Result<()> {
                 skip_source_patch,
                 base_ref.as_deref(),
                 state_root.as_deref(),
+                None, // `apply` aplica un .swm genérico: no conoce el nombre del paquete
             )?;
         }
         Cmd::SwmVerify { file, base_ref, trust } => {
@@ -1326,9 +1327,19 @@ fn run_pack(
         Some(buf)
     };
 
-    let target_bin = target_bin
-        .map(|s| s.to_string())
-        .unwrap_or_else(|| format!("/usr/bin/{}", recipe.name));
+    // target_bin: `--target-bin` manda. Si no, y la receta Cargo declara `--bin <X>`, el binario
+    // instalado es X (no el nombre del paquete: ripgrep→rg, repgrep→rgr) ⇒ derivamos de ahí. Sólo
+    // si no hay `--bin`, caemos a `/usr/bin/{name}` (correcto para C y para name==bin).
+    let target_bin = target_bin.map(|s| s.to_string()).unwrap_or_else(|| {
+        let bin = recipe
+            .build
+            .flags
+            .windows(2)
+            .find(|w| w[0] == "--bin")
+            .map(|w| w[1].clone())
+            .unwrap_or_else(|| recipe.name.clone());
+        format!("/usr/bin/{bin}")
+    });
 
     // expected_hash: `--expected` manda; si no, `--build` lo sella construyendo de verdad en el
     // lab; si ninguno, el paquete sale sin ancla de verificación (válido, sólo más débil).
@@ -1624,6 +1635,7 @@ fn run_install(
         skip_source_patch,
         base_ref,
         state_root,
+        Some(&pkg_name), // reproduce con el nombre del paquete ⇒ cache-hit del corpus, sin duplicar
     )?;
 
     // Registrar en la DB de instalados (salvo dry-run de schema). Permite `hammer uninstall`.
@@ -1981,6 +1993,10 @@ fn run_apply(
     skip_source_patch: bool,
     base_ref: Option<&std::path::Path>,
     state_root: Option<&std::path::Path>,
+    // Nombre del paquete (sólo lo sabe `install`): si está, la reproducción del source_patch usa
+    // ESE nombre para la receta sintetizada ⇒ cache-hitea el artefacto del corpus (`<hash>-name`)
+    // en vez de derivar el nombre del target_bin y sellar un duplicado (ripgrep→rg). `apply` pasa None.
+    pkg_name: Option<&str>,
 ) -> anyhow::Result<Vec<PathBuf>> {
     let swm = load_swm(file)?;
     swm.verify_schema()?;
@@ -2020,7 +2036,7 @@ fn run_apply(
                 }
                 let store = hammer_core::Store::open(store_path)?;
                 let cfg = hammer_build::BuildConfig::from_env_or_defaults(store.root());
-                let hash = hammer_build::build_source_patch(m, &cfg, &store, None)?;
+                let hash = hammer_build::build_source_patch(m, &cfg, &store, None, pkg_name)?;
                 let artifact = store.find_by_hash(hash.as_str())?;
                 let into = hammer_core::apply::rebase_path("/", prefix);
                 let report = hammer_build::run_hydrate(
diff --git a/crates/hammerd/src/bus.rs b/crates/hammerd/src/bus.rs
index 83fec07f..84a28116 100644
--- a/crates/hammerd/src/bus.rs
+++ b/crates/hammerd/src/bus.rs
@@ -392,7 +392,7 @@ fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender<Event>) {
         }
     };
     let cfg = hammer_build::BuildConfig::from_env_or_defaults(store.root());
-    match hammer_build::build_source_patch(&mutation, &cfg, &store, None) {
+    match hammer_build::build_source_patch(&mutation, &cfg, &store, None, None) {
         Ok(h) => {
             let _ = tx.send(Event::BuildReady {
                 recipe: name,
diff --git a/scripts/product-userland-from-repo.sh b/scripts/product-userland-from-repo.sh
new file mode 100755
index 00000000..c1e03e78
--- /dev/null
+++ b/scripts/product-userland-from-repo.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+# product-userland-from-repo.sh — arma el USERLAND del producto HIDRATÁNDOLO desde el REPO FIRMADO
+# (Etapa F+G, cierre del lazo): por cada paquete corre `hammer install --repo <repo> --prefix <rfs>
+# --require-signed`, que verifica la firma del release, reproduce desde el `.swm` (cache-hit si ya
+# está en el store) e hidrata el FHS bajo el prefix. El resultado es un árbol de userland cuya cadena
+# de suministro entera viene del repo firmado, no del bootstrap hardcodeado.
+#
+# Uso:   ./scripts/product-userland-from-repo.sh [pkg...]     # default: set curado
+#        PKGS="bat fd jq" ./scripts/product-userland-from-repo.sh
+#        OVER=work/product-image/rootfs ./scripts/product-userland-from-repo.sh   # hidratar SOBRE un rootfs base
+# Env:   REPO (def dist/repo)  TRUST (def dist/keys)  RFS (def work/product-userland)  STORE (def ./store)
+#        OVER (si está, hidrata SOBRE ese dir en vez de uno limpio — p.ej. el product-rootfs base)
+set -euo pipefail
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
+REPO="${REPO:-dist/repo}"; TRUST="${TRUST:-dist/keys}"; STORE="${STORE:-./store}"
+RFS="${OVER:-${RFS:-work/product-userland}}"
+DB="$RFS/var/lib/hammer/installed.json"
+
+# Set curado por defecto: el userland CLI "real" que hace usable a la distro. Todos deben estar en el
+# índice del repo firmado. Editá/pasá args para cambiarlo.
+DEFAULT_PKGS="bat fd ripgrep jq zoxide dust procs htop less tree hyperfine delta eza sd tokei fzf skim git-cliff gitui starship"
+PKGS_LIST="${PKGS:-${*:-$DEFAULT_PKGS}}"
+
+[ -n "${OVER:-}" ] || { rm -rf "$RFS"; mkdir -p "$RFS"; }
+mkdir -p "$(dirname "$DB")"
+
+echo "==> hidratando userland desde el repo firmado ($REPO) → $RFS"
+ok=0; skip=0; fail=0; got=""
+for pkg in $PKGS_LIST; do
+  # ¿está en el índice del repo? (evita abortar por un nombre ausente)
+  if ! grep -q "\"$pkg\"" "$REPO/index.json" 2>/dev/null; then
+    printf "  - %-16s (no está en el repo, salteado)\n" "$pkg"; skip=$((skip+1)); continue
+  fi
+  if NO_PROXY=localhost NIX_STORE="${NIX_STORE:-$HOME/.nixstore}" \
+       "$ROOT/target/release/hammer" --store "$STORE" install "$pkg" \
+       --repo "$REPO" --prefix "$RFS" --trust "$TRUST" --require-signed --db "$DB" \
+       >/dev/null 2>"$RFS/.install-$pkg.log"; then
+    printf "  ✓ %-16s\n" "$pkg"; ok=$((ok+1)); got="$got $pkg"
+  else
+    printf "  ✗ %-16s (ver %s)\n" "$pkg" "$RFS/.install-$pkg.log"; fail=$((fail+1))
+  fi
+done
+echo "==> userland: $ok hidratados, $skip ausentes, $fail fallaron"
+
+# verificación: cada binario presente + estático
+echo "==> verificación (estático + presente):"
+for pkg in $got; do
+  b=$(find "$RFS/usr/bin" -maxdepth 1 -type f \( -name "$pkg" -o -name "*$pkg*" \) 2>/dev/null | head -1)
+  [ -n "$b" ] && printf "  %-16s %s\n" "$pkg" "$(file -b "$b" 2>/dev/null | grep -oE 'statically linked|dynamically linked' || echo '?')"
+done
+rm -f "$RFS"/.install-*.log 2>/dev/null || true
+echo "==> binarios en $RFS/usr/bin: $(ls "$RFS/usr/bin" 2>/dev/null | wc -l)"