build: vendoring con --locked condicional al Cargo.lock committeado (Opción 2)

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) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-06-11 06:21:08 +00:00
co-authored by Claude Opus 4.8
parent 4895c1c884
commit 8d98a6053a
+49 -5
View File
@@ -189,14 +189,33 @@ fn verify_sha256(path: &Path, expected: &str) -> hammer_core::Result<()> {
/// Vendoriza las dependencias de un proyecto Cargo en `<src>/vendor` y escribe /// 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`. /// `<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 /// La red se usa AQUÍ (fuera del sandbox, como el resto del fetch).
/// 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 /// **`--locked` condicional:** si la fuente trae un `Cargo.lock` committeado, se vendorea con
/// la receta). Un crate sin deps de crates.io produce un config vacío y ningún `vendor/`: inocuo. /// `--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<()> { 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") let output = Command::new("cargo")
.current_dir(src) .current_dir(src)
.args(["vendor", "--locked", "vendor"]) .args(&args)
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
.output() .output()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn cargo vendor: {e}")))?; .map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn cargo vendor: {e}")))?;
@@ -286,6 +305,31 @@ mod tests {
vendor_cargo_deps(src).unwrap(); vendor_cargo_deps(src).unwrap();
assert!(src.join(".cargo/config.toml").is_file(), "debe escribir el config de vendoring"); 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<()> { fn run_git(args: &[&str], cwd: Option<&Path>) -> hammer_core::Result<()> {