Etapa F paquetería #3: deps entre paquetes — install resuelve el cierre y reconstruye el catálogo
Cierra el hueco que pack/install advertían: un paquete con build-deps (bwrap→libcap,
openssh→zlib,openssl) ahora se instala por nombre reproduciéndose desde fuente CON sus deps.
Modelo: las build-deps viajan por NOMBRE en el source_patch y en la PackageEntry; install
resuelve el cierre transitivo desde el índice y reconstruye un catálogo de recetas que el lab
consulta al materializar deps en el sandbox.
- hammer-core: `Mutation::SourcePatch.deps` (Deps, serde-skip si vacío) + `from_recipe` lo
carga. `Deps::is_empty`. `RepoIndex`/`PackageEntry.deps` + `resolve_closure(name)` (DFS
topológico, deps antes que dependientes, detecta dep faltante y ciclo). 8 tests nuevos.
- hammer-build/swm_bridge: refactor — `recipe_from_source_patch` (síntesis pública, setea deps
+ base_dir=catálogo) + `catalog_dir_for` (dir determinista compartido). build_source_patch
lo reusa. synthesize_recipe ahora setea recipe.deps + base_dir al catálogo (no "/").
- hammer-cli: pack puebla PackageEntry.deps; install resuelve el cierre y escribe un {dep}.toml
por dep en el catalog_dir ANTES de aplicar el target (mismo dir determinista que usa
build_source_patch ⇒ el lab resuelve {dep}.toml por nombre). Warning de pack actualizado.
- VALIDADO E2E REAL contra ./store: `install bwrap` resuelve libcap del catálogo y reproduce
el artefacto CACHEADO EXACTO (b3:f89e716…) → hidrata bwrap (1.8MB ELF). El paquete con dep
hashea bit-idéntico al original. Resolución/diamante/faltante/ciclo unit-tested. 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -27,77 +27,15 @@ pub fn build_source_patch(
|
||||
store: &Store,
|
||||
scratch_root: Option<&Path>,
|
||||
) -> hammer_core::Result<ArtifactHash> {
|
||||
let (repo, commit, tarball, sha256, strip_components, patch, patch_url, build_cfg, target_bin, expected) =
|
||||
match mutation {
|
||||
Mutation::SourcePatch {
|
||||
repo,
|
||||
commit,
|
||||
tarball,
|
||||
sha256,
|
||||
strip_components,
|
||||
patch,
|
||||
patch_url,
|
||||
build,
|
||||
target_bin,
|
||||
expected_hash,
|
||||
} => (
|
||||
repo, commit, tarball, sha256, strip_components, patch, patch_url, build, target_bin,
|
||||
expected_hash,
|
||||
),
|
||||
_ => {
|
||||
return Err(hammer_core::Error::Recipe(
|
||||
"build_source_patch: la mutación no es 'source_patch'".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let catalog_dir = catalog_dir_for(cfg, scratch_root);
|
||||
let recipe = recipe_from_source_patch(mutation, None, &catalog_dir)?;
|
||||
|
||||
// git xor tarball — misma regla que el schema (`swm_source_kind`).
|
||||
let source_kind = hammer_core::swm::swm_source_kind(
|
||||
repo.as_deref(),
|
||||
commit.as_deref(),
|
||||
tarball.as_deref(),
|
||||
sha256.as_deref(),
|
||||
)
|
||||
.map_err(hammer_core::Error::Recipe)?;
|
||||
|
||||
let scratch = scratch_root.unwrap_or(&cfg.work_root).to_path_buf();
|
||||
let recipe_dir = scratch.join("swm-recipes");
|
||||
std::fs::create_dir_all(&recipe_dir)?;
|
||||
|
||||
// Clave estable del origen para nombrar artefactos a disco: el commit (git) o el
|
||||
// sha256 (tarball). Idéntica reaplicación ⇒ mismo nombre ⇒ hash de receta determinista.
|
||||
let source_key: &str = match &source_kind {
|
||||
hammer_core::SourceKind::Git { commit, .. } => commit,
|
||||
hammer_core::SourceKind::Tarball { sha256, .. } => sha256,
|
||||
let expected = match mutation {
|
||||
Mutation::SourcePatch { expected_hash, .. } => expected_hash.clone(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Materializamos el patch a disco bajo un nombre derivado de la clave de origen (estable:
|
||||
// si el .swm se reaplica, reusamos el mismo archivo y el hash de la receta no depende de
|
||||
// aleatoriedad). Origen: inline (`patch`) o remoto (`patch_url`); el schema garantiza que
|
||||
// no vengan ambos.
|
||||
let mut patches: Vec<String> = Vec::new();
|
||||
let patch_path = recipe_dir.join(format!("{source_key}.patch"));
|
||||
if let Some(text) = patch {
|
||||
std::fs::write(&patch_path, text.as_bytes())?;
|
||||
patches.push(patch_path.to_string_lossy().into_owned());
|
||||
} else if let Some(url) = patch_url {
|
||||
tracing::info!(%url, "fetch: descargando patch remoto");
|
||||
crate::download::fetch_url_to_file(url, &patch_path)?;
|
||||
patches.push(patch_path.to_string_lossy().into_owned());
|
||||
}
|
||||
|
||||
let name = derive_name(target_bin);
|
||||
let recipe = synthesize_recipe(
|
||||
&name,
|
||||
&source_kind,
|
||||
build_cfg,
|
||||
*strip_components,
|
||||
patches,
|
||||
&recipe_dir,
|
||||
)?;
|
||||
|
||||
let hash = build(&recipe, cfg, store)?;
|
||||
if let Some(want) = expected {
|
||||
if let Some(want) = &expected {
|
||||
let want_hex = want.strip_prefix("b3:").unwrap_or(want);
|
||||
let got_hex = hash
|
||||
.as_str()
|
||||
@@ -112,6 +50,85 @@ pub fn build_source_patch(
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
/// El directorio-catálogo donde se materializan los patches y los `{dep}.toml`. Determinista a
|
||||
/// partir de `cfg.work_root` (o `scratch_root`): por eso `install` puede pre-poblarlo con los
|
||||
/// `{dep}.toml` y `build_source_patch` los encontrará al resolver build-deps del mismo `base_dir`.
|
||||
pub fn catalog_dir_for(cfg: &BuildConfig, scratch_root: Option<&Path>) -> PathBuf {
|
||||
scratch_root.unwrap_or(&cfg.work_root).join("swm-recipes")
|
||||
}
|
||||
|
||||
/// Sintetiza (SIN construir) la `Recipe` efímera de un `source_patch`: parsea el origen,
|
||||
/// materializa el patch inline/remoto a disco bajo `catalog_dir`, y arma la receta con sus
|
||||
/// `deps` y `base_dir = catalog_dir` (para que las build-deps resuelvan `{dep}.toml` ahí).
|
||||
///
|
||||
/// `name` sobreescribe el nombre derivado del `target_bin`. Es OBLIGATORIO al escribir un
|
||||
/// `{dep}.toml`: el nombre de la receta DEBE ser el de la dep para que el dependiente la resuelva.
|
||||
pub fn recipe_from_source_patch(
|
||||
mutation: &Mutation,
|
||||
name: Option<&str>,
|
||||
catalog_dir: &Path,
|
||||
) -> hammer_core::Result<Recipe> {
|
||||
let (repo, commit, tarball, sha256, strip_components, patch, patch_url, build_cfg, target_bin, deps) =
|
||||
match mutation {
|
||||
Mutation::SourcePatch {
|
||||
repo, commit, tarball, sha256, strip_components, patch, patch_url, build,
|
||||
target_bin, deps, ..
|
||||
} => (
|
||||
repo, commit, tarball, sha256, strip_components, patch, patch_url, build,
|
||||
target_bin, deps,
|
||||
),
|
||||
_ => {
|
||||
return Err(hammer_core::Error::Recipe(
|
||||
"recipe_from_source_patch: la mutación no es 'source_patch'".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// git xor tarball — misma regla que el schema (`swm_source_kind`).
|
||||
let source_kind = hammer_core::swm::swm_source_kind(
|
||||
repo.as_deref(),
|
||||
commit.as_deref(),
|
||||
tarball.as_deref(),
|
||||
sha256.as_deref(),
|
||||
)
|
||||
.map_err(hammer_core::Error::Recipe)?;
|
||||
|
||||
std::fs::create_dir_all(catalog_dir)?;
|
||||
|
||||
// Clave estable del origen para nombrar el patch a disco: el commit (git) o el sha256
|
||||
// (tarball). Idéntica reaplicación ⇒ mismo nombre ⇒ hash de receta determinista.
|
||||
let source_key: &str = match &source_kind {
|
||||
hammer_core::SourceKind::Git { commit, .. } => commit,
|
||||
hammer_core::SourceKind::Tarball { sha256, .. } => sha256,
|
||||
};
|
||||
|
||||
let mut patches: Vec<String> = Vec::new();
|
||||
if patch.is_some() || patch_url.is_some() {
|
||||
// Patch bajo un nombre derivado del nombre-de-receta + clave de origen, para no colisionar
|
||||
// entre `{dep}.patch` distintos en el mismo catálogo.
|
||||
let label = name.map(|n| n.to_string()).unwrap_or_else(|| derive_name(target_bin));
|
||||
let patch_path = catalog_dir.join(format!("{label}-{source_key}.patch"));
|
||||
if let Some(text) = patch {
|
||||
std::fs::write(&patch_path, text.as_bytes())?;
|
||||
} else if let Some(url) = patch_url {
|
||||
tracing::info!(%url, "fetch: descargando patch remoto");
|
||||
crate::download::fetch_url_to_file(url, &patch_path)?;
|
||||
}
|
||||
patches.push(patch_path.to_string_lossy().into_owned());
|
||||
}
|
||||
|
||||
let name = name.map(|n| n.to_string()).unwrap_or_else(|| derive_name(target_bin));
|
||||
synthesize_recipe(
|
||||
&name,
|
||||
&source_kind,
|
||||
build_cfg,
|
||||
*strip_components,
|
||||
deps,
|
||||
patches,
|
||||
catalog_dir,
|
||||
)
|
||||
}
|
||||
|
||||
/// Nombre legible para el directorio del store. `/usr/bin/grep` → `grep`. Si el path no
|
||||
/// tiene basename (raro), usamos `swm-bin`. El nombre afecta SÓLO al sufijo legible
|
||||
/// (`<hash>-<name>`), no al hash; cambiarlo no rompe la caché del store.
|
||||
@@ -123,11 +140,13 @@ fn derive_name(target_bin: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn synthesize_recipe(
|
||||
name: &str,
|
||||
source: &hammer_core::SourceKind<'_>,
|
||||
build_cfg: &SwmBuild,
|
||||
strip_components: Option<usize>,
|
||||
deps: &hammer_core::Deps,
|
||||
patches: Vec<String>,
|
||||
base_dir: &Path,
|
||||
) -> hammer_core::Result<Recipe> {
|
||||
@@ -188,7 +207,12 @@ flags = []
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
recipe.base_dir = PathBuf::from("/"); // patches absolutos: base_dir es irrelevante
|
||||
// Deps por NOMBRE (de la receta original): el lab las resolverá como `{dep}.toml` bajo el
|
||||
// `base_dir` — por eso base_dir = el catálogo (no "/"): los patches son absolutos y no lo
|
||||
// necesitan, pero la resolución de build-deps SÍ. `install` puebla el catálogo con los
|
||||
// `{dep}.toml` antes de construir; sin deps, el dir simplemente no se consulta.
|
||||
recipe.deps = deps.clone();
|
||||
recipe.base_dir = base_dir.to_path_buf();
|
||||
Ok(recipe)
|
||||
}
|
||||
|
||||
@@ -246,6 +270,7 @@ mod tests {
|
||||
},
|
||||
&fake_swm_build(),
|
||||
None,
|
||||
&hammer_core::Deps::default(),
|
||||
vec![],
|
||||
d.path(),
|
||||
)
|
||||
@@ -273,6 +298,7 @@ mod tests {
|
||||
},
|
||||
&fake_swm_build(),
|
||||
None,
|
||||
&hammer_core::Deps::default(),
|
||||
vec![],
|
||||
d.path(),
|
||||
)
|
||||
@@ -329,13 +355,16 @@ mod tests {
|
||||
build: fake_swm_build(),
|
||||
target_bin: "/bin/x".into(),
|
||||
expected_hash: None,
|
||||
deps: Default::default(),
|
||||
};
|
||||
let store = Store::open(d.path().join("store")).unwrap();
|
||||
let cfg = BuildConfig::defaults_for_store(store.root());
|
||||
let scratch = d.path().join("scratch");
|
||||
// Falla (no hay rootfs), pero el patch ya debe estar descargado.
|
||||
let _ = build_source_patch(&m, &cfg, &store, Some(&scratch));
|
||||
let landed = scratch.join("swm-recipes").join(format!("{commit}.patch"));
|
||||
// El patch se materializa como `{label}-{source_key}.patch` (label = derive_name del
|
||||
// target_bin) para no colisionar entre deps en el catálogo.
|
||||
let landed = scratch.join("swm-recipes").join(format!("x-{commit}.patch"));
|
||||
assert!(landed.is_file(), "el patch remoto debió descargarse a {}", landed.display());
|
||||
assert_eq!(std::fs::read(&landed).unwrap(), b"--- a\n+++ b\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user