build: vendoring de deps Cargo en el fetch para builds --offline (Lote 6)

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) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-06-11 01:04:46 +00:00
co-authored by Claude Opus 4.8
parent 60a9451a03
commit e9b0551213
2 changed files with 71 additions and 0 deletions
+64
View File
@@ -186,6 +186,35 @@ fn verify_sha256(path: &Path, expected: &str) -> hammer_core::Result<()> {
Ok(())
}
/// Vendoriza las dependencias de un proyecto Cargo en `<src>/vendor` y escribe
/// `<src>/.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 {
+7
View File
@@ -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) {