Etapa G build-yield: imports Rust de nix BUILDEAN (aislamiento [workspace] genérico + filtro toolchain)

Midiendo build-yield real con la capa puesta: hyperfine (nix) construye end-to-end → ELF estático
musl que corre. Dos fixes que lo desbloquean genéricamente (sin patch por receta):

- nix_import.rs: rustc/cargo/rust se filtran de deps (son el LAB, no paquetes) — sin esto el build
  abortaba buscando rustc.toml. Default de flags Rust vuelve a `--bin <bin>` (el `-p <pname>` no
  generaliza: el paquete cargo del bin puede ≠ pname, p.ej. sd→sd-cli).
- hammer-build/lib.rs: `ensure_cargo_workspace_isolation` inyecta `[workspace]` vacío al Cargo.toml
  de la fuente si no lo tiene, ANTES de vendor. Idempotente ⇒ no choca con las recetas del corpus
  que lo parchean a mano. Resuelve el gotcha "fuente dentro del workspace hammer ⇒ cargo vendor
  aborta" para CUALQUIER import Rust.

BUILD-YIELD medido (real, con la capa): lz4 (C/Alpine, escape gcc) ✓ · hyperfine (Rust/nix) ✓ ·
sd (Rust) ✗ workspace-virtual con bin en paquete ≠pname (necesita `-p` manual). Texture honesta:
los bien-estructurados buildean solos; los con quirks de workspace necesitan toque per-paquete.
31 suites verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 08:31:56 -04:00
co-authored by Claude Opus 4.8
parent c3f890c7d1
commit 6e4d8da779
2 changed files with 41 additions and 4 deletions
+31
View File
@@ -173,6 +173,13 @@ pub fn build(
// 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) {
// El source se copia DENTRO del repo hammer (que es un workspace Cargo) ⇒ si el Cargo.toml
// de la fuente NO declara su propio `[workspace]`, cargo cree que pertenece al workspace de
// hammer y `cargo vendor` aborta. Inyectamos un `[workspace]` vacío para aislarla. SÓLO si
// no lo tiene ya (las recetas del corpus que lo parchean a mano siguen funcionando sin
// 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)?;
tracing::info!("cargo: vendoreando deps para build offline");
fetch::vendor_cargo_deps(&src_tree)?;
}
@@ -273,6 +280,30 @@ enum BuildSys {
Unknown,
}
/// 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
/// lo parchea), no toca nada. Caso virtual-manifest (root ya es `[workspace]`) tampoco se toca.
fn ensure_cargo_workspace_isolation(src: &Path) -> hammer_core::Result<()> {
let manifest = src.join("Cargo.toml");
let text = std::fs::read_to_string(&manifest)?;
// Heurística simple: ¿hay una tabla `[workspace]` (al inicio de línea)? Cubre `[workspace]` y
// `[workspace.members]`/`[workspace.dependencies]` (un root virtual siempre trae alguna).
let has_workspace = text
.lines()
.any(|l| l.trim_start().starts_with("[workspace]") || l.trim_start().starts_with("[workspace."));
if !has_workspace {
let mut patched = text;
if !patched.ends_with('\n') {
patched.push('\n');
}
patched.push_str("\n[workspace]\n");
std::fs::write(&manifest, patched)?;
tracing::info!("cargo: inyectado [workspace] vacío para aislar la fuente del repo hammer");
}
Ok(())
}
fn detect_build_system(src: &Path) -> BuildSys {
let has = |p: &str| src.join(p).exists();
// El orden importa: cuando coexisten varios (un tarball release de autotools trae el
+10 -4
View File
@@ -172,6 +172,9 @@ pub fn to_recipe_toml(pkg: &NixPkg) -> Result<String, String> {
// El bin = meta.mainProgram de nix (ripgrep→rg), o el pname. Esto deja la receta build-ready.
let (flags_line, rust_install) = if pkg.is_rust {
let bin = if pkg.main_program.is_empty() { name.clone() } else { sanitize_name(&pkg.main_program) };
// `--bin <bin>`: default para el caso común (crate suelto, pname=paquete cargo). Los repos
// con WORKSPACE VIRTUAL en el root (p.ej. sd: el bin vive en el paquete `sd-cli`≠pname)
// necesitan que el humano agregue `-p <paquete-real>` — el import no puede adivinar el nombre.
(
format!("flags = [\"--bin\", \"{bin}\"]\n"),
format!(
@@ -230,6 +233,10 @@ fn is_nix_noise(name: &str) -> bool {
|| name == "auditable-cargo"
|| name.starts_with("auditable-cargo-")
|| name == "version-check"
// El toolchain Rust ES el lab de hammer (cargo vendorea las crates), NO un paquete dep.
|| name == "rustc"
|| name == "cargo"
|| name == "rust"
}
/// Nombre de receta válido: minúsculas, sólo `[a-z0-9._-]`, sin espacios. nix usa nombres limpios
@@ -420,10 +427,9 @@ mod tests {
}"#;
let pkg: NixPkg = serde_json::from_str(json).unwrap();
let recipe = hammer_core::Recipe::from_toml(&to_recipe_toml(&pkg).unwrap()).unwrap();
// pcre2 y rustc sobreviven (paquetes reales); los hooks/wrappers/shell-files se filtran.
// Orden = inserción (native primero, luego build): los native son todos ruido ⇒ quedan
// pcre2, rustc en el orden de build_inputs.
assert_eq!(recipe.deps.build, vec!["pcre2", "rustc"]);
// pcre2 sobrevive (paquete real); hooks/wrappers/shell-files y el toolchain rustc/cargo
// se filtran (rustc/cargo son el lab, no deps).
assert_eq!(recipe.deps.build, vec!["pcre2"]);
}
#[test]