gioser

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 1389Shell 137Markdown 80Rust 802602 Definiciones

Etapa G Fase 1: extractor de workspaces Rust — `-p <pkg>` auto en workspaces virtuales

Sube el yield Rust a casi-todo el ecosistema CLI moderno (sd/fd/… usan workspace virtual: root
sólo agrupa, el bin vive en un sub-paquete ≠ pname). Antes exigía `-p` manual; ahora el lab lo
resuelve solo.

- hammer-build/lib.rs: cargo_root_is_virtual (root con [workspace] sin [package]) +
  resolve_cargo_bin_package (lee los members vía `cargo metadata` —fuente autoritativa: globs,
  [[bin]], src/bin/*, nested— y devuelve el paquete que expone el bin) + inject_cargo_package_selector
  (si virtual y flags piden --bin X sin -p, antepone `-p <pkg>`). Determinista ⇒ reproducible; el
  hash usa los flags ORIGINALES, el -p es resolución interna. Crate suelto / ripgrep: intactos
  (no virtual). serde_json a deps de hammer-build.
- VALIDADO REAL: sd (workspace virtual, bin en `sd-cli`≠pname) ahora BUILDEA SOLO (sd 1.0.0),
  sin tocar la receta (flags quedan --bin sd, el lab resuelve -p sd-cli). 31 suites verde.

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

a157b8c455979b282a9d1f1ba8a5fb900ab562f4 · por sergio · (hace 4 meses) · padre 6e4d8da77 · ver el árbol en este commit

Delta semántico · 4

crates/hammer-build/src/lib.rs

3 archivos · +89 −1

diff --git a/Cargo.lock b/Cargo.lock
index 327194cd..5feba38d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -524,6 +524,7 @@ dependencies = [
  "anyhow",
  "hammer-core",
  "hex",
+ "serde_json",
  "sha2",
  "tempfile",
  "thiserror",
diff --git a/crates/hammer-build/Cargo.toml b/crates/hammer-build/Cargo.toml
index 268d3607..d64e2c76 100644
--- a/crates/hammer-build/Cargo.toml
+++ b/crates/hammer-build/Cargo.toml
@@ -14,6 +14,7 @@ thiserror.workspace = true
 tracing.workspace = true
 sha2.workspace = true
 hex.workspace = true
+serde_json.workspace = true
 
 [dev-dependencies]
 tempfile.workspace = true
diff --git a/crates/hammer-build/src/lib.rs b/crates/hammer-build/src/lib.rs
index d2f3d02c..3682913c 100644
--- a/crates/hammer-build/src/lib.rs
+++ b/crates/hammer-build/src/lib.rs
@@ -170,6 +170,12 @@ pub fn build(
         fetch::apply_patches(recipe, &src_tree)?;
     }
 
+    // Receta efectiva para resolver las FASES: puede ganar un `-p <pkg>` si la fuente es un
+    // workspace Cargo virtual (ver inject_cargo_package_selector). El hash `h` ya está fijado desde
+    // los flags ORIGINALES de la receta; el `-p` es resolución determinista del lab (misma fuente →
+    // mismo paquete) ⇒ no afecta la reproducibilidad ni el sidecar (que guarda la receta original).
+    let mut eff_recipe = recipe.clone();
+
     // Recetas Cargo: vendoreamos las deps (red permitida en el fetch) para que el build sea
     // hermético `--offline` dentro del sandbox. Tras los patches, por si tocaron Cargo.toml/lock.
     if matches!(detect_build_system(&src_tree), BuildSys::Cargo) {
@@ -180,6 +186,8 @@ pub fn build(
         // duplicar la tabla). Esto desbloquea genéricamente los imports Rust (Etapa G) sin un patch
         // por receta. Ver memoria 'cargo-recipe crate suelto'.
         ensure_cargo_workspace_isolation(&src_tree)?;
+        // Extractor de workspaces (Etapa G): si el root es virtual, resuelve `-p <pkg>` del bin.
+        inject_cargo_package_selector(&mut eff_recipe, &src_tree);
         tracing::info!("cargo: vendoreando deps para build offline");
         fetch::vendor_cargo_deps(&src_tree)?;
     }
@@ -222,7 +230,7 @@ pub fn build(
         env,
     };
 
-    let phases = resolve_phases(recipe, &src_tree)?;
+    let phases = resolve_phases(&eff_recipe, &src_tree)?;
     if let Some(cmd) = &phases.configure {
         tracing::info!(cmd, "phase: configure");
         sb.run(cmd)?;
@@ -280,6 +288,84 @@ enum BuildSys {
     Unknown,
 }
 
+/// ¿El `Cargo.toml` raíz es un MANIFIESTO VIRTUAL? (declara `[workspace]` pero NO `[package]`).
+/// Es el patrón del ecosistema CLI Rust moderno (sd, fd, …): el root sólo agrupa miembros y el
+/// binario vive en un sub-paquete. En ese caso `cargo … --bin X` solo falla: hace falta `-p <pkg>`.
+/// Un crate suelto (con `[package]`, aunque `ensure_cargo_workspace_isolation` le agregue
+/// `[workspace]`) NO es virtual ⇒ no se toca (ripgrep y el corpus quedan idénticos).
+fn cargo_root_is_virtual(src: &Path) -> bool {
+    let Ok(text) = std::fs::read_to_string(src.join("Cargo.toml")) else {
+        return false;
+    };
+    let mut has_ws = false;
+    let mut has_pkg = false;
+    for l in text.lines() {
+        let t = l.trim_start();
+        if t.starts_with("[workspace]") || t.starts_with("[workspace.") {
+            has_ws = true;
+        }
+        if t.starts_with("[package]") {
+            has_pkg = true;
+        }
+    }
+    has_ws && !has_pkg
+}
+
+/// Lee los MEMBERS del workspace (vía `cargo metadata`, la fuente autoritativa: expande globs,
+/// `[[bin]]`, bins por convención `src/bin/*.rs`, y workspaces anidados — todo lo que un parseo a
+/// mano del TOML erraría) y devuelve el NOMBRE del paquete que expone un bin llamado `bin`. Es el
+/// "extractor de workspaces" de la Etapa G: convierte un workspace virtual en un `-p <pkg>` exacto.
+fn resolve_cargo_bin_package(src: &Path, bin: &str) -> Option<String> {
+    let out = std::process::Command::new("cargo")
+        .args(["metadata", "--no-deps", "--format-version", "1"])
+        .current_dir(src)
+        .output()
+        .ok()?;
+    if !out.status.success() {
+        return None;
+    }
+    let meta: serde_json::Value = serde_json::from_slice(&out.stdout).ok()?;
+    for pkg in meta.get("packages")?.as_array()? {
+        let pkg_name = pkg.get("name")?.as_str()?;
+        for tgt in pkg.get("targets")?.as_array()? {
+            let is_bin = tgt
+                .get("kind")
+                .and_then(|k| k.as_array())
+                .map(|ks| ks.iter().any(|x| x.as_str() == Some("bin")))
+                .unwrap_or(false);
+            if is_bin && tgt.get("name").and_then(|n| n.as_str()) == Some(bin) {
+                return Some(pkg_name.to_string());
+            }
+        }
+    }
+    None
+}
+
+/// Si la fuente es un workspace VIRTUAL y los flags piden `--bin X` sin `-p`, resuelve el paquete
+/// real que expone `X` y antepone `-p <pkg>`. Deja los flags intactos en cualquier otro caso
+/// (crate suelto, ya con `-p`, o bin irresoluble). Determinista (misma fuente → mismo paquete) ⇒
+/// reproducible. Sin esto, casi todo el CLI Rust moderno (workspaces virtuales) exigía `-p` a mano.
+fn inject_cargo_package_selector(recipe: &mut Recipe, src: &Path) {
+    let flags = &recipe.build.flags;
+    if flags.iter().any(|f| f == "-p" || f == "--package") {
+        return; // el autor ya eligió el paquete
+    }
+    if !cargo_root_is_virtual(src) {
+        return; // crate suelto: `--bin` solo alcanza
+    }
+    // bin = el argumento que sigue a `--bin`.
+    let Some(bin) = flags.iter().position(|f| f == "--bin").and_then(|i| flags.get(i + 1)).cloned()
+    else {
+        return;
+    };
+    if let Some(pkg) = resolve_cargo_bin_package(src, &bin) {
+        tracing::info!(%bin, %pkg, "cargo: workspace virtual ⇒ -p resuelto automáticamente");
+        let mut new_flags = vec!["-p".to_string(), pkg];
+        new_flags.extend(recipe.build.flags.iter().cloned());
+        recipe.build.flags = new_flags;
+    }
+}
+
 /// Garantiza que el `Cargo.toml` raíz de la fuente declare un `[workspace]` propio, para que cargo
 /// NO la considere parte del workspace del repo hammer (que la contiene en `work/sources/`). Append
 /// idempotente: si ya hay `[workspace]` (crate suelto que es su propio workspace, o una receta que