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>
This commit is contained in:
Generated
+1
@@ -524,6 +524,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"hammer-core",
|
||||
"hex",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user