tawasuyu

sergio/takana

> 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…

TOML 1373Shell 116Rust 74Markdown 641915 Sutichaykuna

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>

e9b055121322d3ef8aef8e75738052d5aff0693a · ruwaq Sergio · (4 killa ñawpaq) · tayta 60a9451a0 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 4

crates/hammer-build/src/fetch.rs

crates/hammer-build/src/lib.rs

  • ukhunpi t'ikrakurqan fn build pub

2 qillqakuna · +71 −0

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 `<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 {
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) {