From d41f1705e697037a693430559816e85455d19965 Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 25 Jun 2026 16:54:25 -0400 Subject: [PATCH] =?UTF-8?q?imagen=E2=86=94repo:=20install=20hidrata=20user?= =?UTF-8?q?land=20del=20repo=20firmado=20+=202=20fixes=20de=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ` 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 --- crates/hammer-bootstrap/src/lib.rs | 2 +- crates/hammer-build/src/swm_bridge.rs | 11 ++++-- crates/hammer-cli/src/main.rs | 24 ++++++++++--- crates/hammerd/src/bus.rs | 2 +- scripts/product-userland-from-repo.sh | 52 +++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 9 deletions(-) create mode 100755 scripts/product-userland-from-repo.sh 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 { 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 (`-`) 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 `, 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 (`-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> { 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) { } }; 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 --prefix +# --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)"