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:
co-authored by
Claude Opus 4.8
parent
60a9451a03
commit
e9b0551213
@@ -186,6 +186,35 @@ fn verify_sha256(path: &Path, expected: &str) -> hammer_core::Result<()> {
|
|||||||
Ok(())
|
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`.
|
/// Aplica los patches de la receta (relativos a `recipe.base_dir`) sobre `tree`.
|
||||||
pub fn apply_patches(recipe: &Recipe, tree: &Path) -> hammer_core::Result<()> {
|
pub fn apply_patches(recipe: &Recipe, tree: &Path) -> hammer_core::Result<()> {
|
||||||
for p in &recipe.source.patches {
|
for p in &recipe.source.patches {
|
||||||
@@ -224,6 +253,41 @@ pub fn apply_patches(recipe: &Recipe, tree: &Path) -> hammer_core::Result<()> {
|
|||||||
Ok(())
|
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<()> {
|
fn run_git(args: &[&str], cwd: Option<&Path>) -> hammer_core::Result<()> {
|
||||||
let mut cmd = Command::new("git");
|
let mut cmd = Command::new("git");
|
||||||
if let Some(c) = cwd {
|
if let Some(c) = cwd {
|
||||||
|
|||||||
@@ -104,6 +104,13 @@ pub fn build(
|
|||||||
fetch::apply_patches(recipe, &src_tree)?;
|
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 out_dir = unique_out_dir(&cfg.work_root, &recipe.name, &h)?;
|
||||||
let mut env: Vec<(String, String)> = Vec::new();
|
let mut env: Vec<(String, String)> = Vec::new();
|
||||||
if matches!(recipe.build.link, LinkMode::Static) {
|
if matches!(recipe.build.link, LinkMode::Static) {
|
||||||
|
|||||||
Reference in New Issue
Block a user