From 8d98a6053aa4d7c4c1e43c49bf084b4314ff4920 Mon Sep 17 00:00:00 2001 From: Sergio Date: Thu, 11 Jun 2026 06:21:08 +0000 Subject: [PATCH] =?UTF-8?q?build:=20vendoring=20con=20--locked=20condicion?= =?UTF-8?q?al=20al=20Cargo.lock=20committeado=20(Opci=C3=B3n=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Desbloquea recetas Cargo cuya fuente no committea Cargo.lock (p.ej. el monorepo tawasuyu, que lo gitignora) sin tocar la política de ese repo. vendor_cargo_deps: si la fuente trae Cargo.lock → `cargo vendor --locked` (reproducible, deps pineadas por el lock del commit). Si no → vendoreo sin --locked (genera el lock al vuelo) + warn! explícito de "build no pineado en el tiempo". El end-state reproducible sigue siendo committear el lock (plan C.2 #5). +1 test del camino sin-lock. Aplica a arje-zero (tawasuyu); hammerd (hammer, que sí committea el lock) sigue por el camino --locked. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/hammer-build/src/fetch.rs | 54 +++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/crates/hammer-build/src/fetch.rs b/crates/hammer-build/src/fetch.rs index 8c0a33b1..76b381e8 100644 --- a/crates/hammer-build/src/fetch.rs +++ b/crates/hammer-build/src/fetch.rs @@ -189,14 +189,33 @@ fn verify_sha256(path: &Path, expected: &str) -> hammer_core::Result<()> { /// Vendoriza las dependencias de un proyecto Cargo en `/vendor` y escribe /// `/.cargo/config.toml` para que el build dentro del sandbox resuelva todo `--offline`. /// -/// La red se usa AQUÍ (fuera del sandbox, como el resto del fetch). `--locked` fija las versiones -/// por el `Cargo.lock` del source: mismo commit ⇒ mismo lock ⇒ mismas deps vendoreadas, así que -/// el vendoring no introduce no-determinismo (el `Cargo.lock` ya está dentro del commit que hashea -/// la receta). Un crate sin deps de crates.io produce un config vacío y ningún `vendor/`: inocuo. +/// La red se usa AQUÍ (fuera del sandbox, como el resto del fetch). +/// +/// **`--locked` condicional:** si la fuente trae un `Cargo.lock` committeado, se vendorea con +/// `--locked` y el build es reproducible (mismo commit ⇒ mismo lock ⇒ mismas deps; el lock ya está +/// dentro del commit que hashea la receta). Si la fuente **no** lo trae (p.ej. un monorepo que lo +/// gitignora, como tawasuyu), se genera el lock al vuelo **sin** `--locked` y se emite un `warn!`: +/// el build queda internamente consistente pero **no pineado en el tiempo** (las deps pueden +/// derivar entre corridas). El end-state reproducible es committear el `Cargo.lock` en la fuente +/// (plan C.2 #5). Ver `docs/runbooks/stage1-vm-boot.md` §8. +/// +/// Un crate sin deps de crates.io produce un config vacío y ningún `vendor/`: inocuo. pub fn vendor_cargo_deps(src: &Path) -> hammer_core::Result<()> { + let locked = src.join("Cargo.lock").is_file(); + let mut args: Vec<&str> = vec!["vendor"]; + if locked { + args.push("--locked"); + } else { + tracing::warn!( + src = %src.display(), + "sin Cargo.lock committeado: vendoreo sin --locked; el build NO es reproducible en el \ + tiempo (deps no pineadas). Committear el lock en la fuente para fijarlo (plan C.2 #5)." + ); + } + args.push("vendor"); let output = Command::new("cargo") .current_dir(src) - .args(["vendor", "--locked", "vendor"]) + .args(&args) .stderr(Stdio::inherit()) .output() .map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn cargo vendor: {e}")))?; @@ -286,6 +305,31 @@ mod tests { vendor_cargo_deps(src).unwrap(); assert!(src.join(".cargo/config.toml").is_file(), "debe escribir el config de vendoring"); } + + #[test] + fn vendor_cargo_deps_without_committed_lock() { + // Fuente SIN Cargo.lock (como un monorepo que lo gitignora): vendoreamos sin --locked, + // generando el lock al vuelo. Para un crate depless es offline. + let tmp = tempfile::tempdir().unwrap(); + let src = tmp.path(); + std::fs::write( + src.join("Cargo.toml"), + "[package]\nname = \"vt\"\nversion = \"0.0.0\"\nedition = \"2021\"\n\n[dependencies]\n", + ) + .unwrap(); + std::fs::create_dir_all(src.join("src")).unwrap(); + std::fs::write(src.join("src/main.rs"), "fn main() {}").unwrap(); + assert!(!src.join("Cargo.lock").is_file(), "precondición: sin lock"); + + if Command::new("cargo").arg("--version").status().map(|s| !s.success()).unwrap_or(true) { + eprintln!("SKIP: cargo no disponible"); + return; + } + // No debe fallar por falta de --locked; genera el lock y escribe el config. + vendor_cargo_deps(src).unwrap(); + assert!(src.join(".cargo/config.toml").is_file()); + assert!(src.join("Cargo.lock").is_file(), "vendor sin --locked genera el lock"); + } } fn run_git(args: &[&str], cwd: Option<&Path>) -> hammer_core::Result<()> {