From e9b055121322d3ef8aef8e75738052d5aff0693a Mon Sep 17 00:00:00 2001 From: Sergio Date: Thu, 11 Jun 2026 01:04:46 +0000 Subject: [PATCH] build: vendoring de deps Cargo en el fetch para builds --offline (Lote 6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completa BuildSys::Cargo: las recetas Rust necesitan sus deps de crates.io disponibles para el `cargo build --offline` hermético del sandbox. fetch:: vendor_cargo_deps corre `cargo vendor --locked` (red OK fuera del sandbox) y escribe .cargo/config.toml en el árbol del source; build() lo invoca cuando detecta una receta Cargo, tras los patches. - --locked fija por Cargo.lock (dentro del commit que hashea la receta) ⇒ sin no-determinismo nuevo: mismo commit ⇒ mismas deps vendoreadas - crate sin deps ⇒ config vacío, inocuo - test gated (skip si no hay cargo) del camino spawn → stdout → config.toml Habilita hammerd (y a futuro arje) como recetas Cargo construibles offline. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/hammer-build/src/fetch.rs | 64 ++++++++++++++++++++++++++++++++ crates/hammer-build/src/lib.rs | 7 ++++ 2 files changed, 71 insertions(+) diff --git a/crates/hammer-build/src/fetch.rs b/crates/hammer-build/src/fetch.rs index d2c49418..8c0a33b1 100644 --- a/crates/hammer-build/src/fetch.rs +++ b/crates/hammer-build/src/fetch.rs @@ -186,6 +186,35 @@ fn verify_sha256(path: &Path, expected: &str) -> hammer_core::Result<()> { Ok(()) } +/// 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. +pub fn vendor_cargo_deps(src: &Path) -> hammer_core::Result<()> { + let output = Command::new("cargo") + .current_dir(src) + .args(["vendor", "--locked", "vendor"]) + .stderr(Stdio::inherit()) + .output() + .map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn cargo vendor: {e}")))?; + if !output.status.success() { + return Err(hammer_core::Error::Other(anyhow::anyhow!( + "cargo vendor falló (exit {:?})", + output.status.code() + ))); + } + // `cargo vendor` emite por stdout el fragmento `[source.*]` a instalar; lo persistimos en el + // árbol del source para que `cargo build --offline` lo lea (ruta relativa `vendor`, que + // resuelve igual dentro del sandbox donde el source se monta en /src). + let cargo_dir = src.join(".cargo"); + std::fs::create_dir_all(&cargo_dir)?; + std::fs::write(cargo_dir.join("config.toml"), &output.stdout)?; + Ok(()) +} + /// Aplica los patches de la receta (relativos a `recipe.base_dir`) sobre `tree`. pub fn apply_patches(recipe: &Recipe, tree: &Path) -> hammer_core::Result<()> { for p in &recipe.source.patches { @@ -224,6 +253,41 @@ pub fn apply_patches(recipe: &Recipe, tree: &Path) -> hammer_core::Result<()> { Ok(()) } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn vendor_cargo_deps_runs_and_writes_config() { + // Crate sin deps de crates.io: `cargo vendor` sale 0 con config vacío y sin red. + // Verifica el camino spawn → captura stdout → escribe `.cargo/config.toml`. + 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(); + + // `--locked` exige un Cargo.lock presente; lo generamos offline (sin deps). + let lock_ok = Command::new("cargo") + .args(["generate-lockfile", "--offline"]) + .current_dir(src) + .status() + .map(|s| s.success()) + .unwrap_or(false); + if !lock_ok { + eprintln!("SKIP: cargo no disponible para generate-lockfile"); + return; + } + + vendor_cargo_deps(src).unwrap(); + assert!(src.join(".cargo/config.toml").is_file(), "debe escribir el config de vendoring"); + } +} + fn run_git(args: &[&str], cwd: Option<&Path>) -> hammer_core::Result<()> { let mut cmd = Command::new("git"); if let Some(c) = cwd { diff --git a/crates/hammer-build/src/lib.rs b/crates/hammer-build/src/lib.rs index fa885a8b..e494c608 100644 --- a/crates/hammer-build/src/lib.rs +++ b/crates/hammer-build/src/lib.rs @@ -104,6 +104,13 @@ pub fn build( fetch::apply_patches(recipe, &src_tree)?; } + // 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) { + tracing::info!("cargo: vendoreando deps para build offline"); + fetch::vendor_cargo_deps(&src_tree)?; + } + let out_dir = unique_out_dir(&cfg.work_root, &recipe.name, &h)?; let mut env: Vec<(String, String)> = Vec::new(); if matches!(recipe.build.link, LinkMode::Static) {