> 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…
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>
diff --git a/crates/hammer-build/src/lib.rs b/crates/hammer-build/src/lib.rs
index b5cea324..d2f3d02c 100644
--- a/crates/hammer-build/src/lib.rs
+++ b/crates/hammer-build/src/lib.rs
@@ -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
diff --git a/crates/hammer-cli/src/nix_import.rs b/crates/hammer-cli/src/nix_import.rs
index 9d421236..f86cf41e 100644
--- a/crates/hammer-cli/src/nix_import.rs
+++ b/crates/hammer-cli/src/nix_import.rs
@@ -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]