hash: el toolchain del lab entra en hash_inputs — el corpus entero se re-hashea
Decision del usuario tras la medicion de los 4 kernels: dos labs con distinto rustc producian bytes distintos en la MISMA direccion, y el store no tenia como notarlo. Ahora el toolchain es una entrada del ArtifactHash. QUE ENTRA: 29 paquetes del rootfs cuya VERSION puede cambiar los bytes — compiladores/enlazadores (gcc, clang, llvm, binutils, rust, cargo), las libs de codegen de gcc (gmp, mpfr4, mpc1, isl), el runtime que se enlaza (musl, libgcc, libstdc++, libatomic, libgomp) y los headers que se compilan dentro (linux-headers, fortify-headers). NO entra el rootfs entero: cada paquete de mas invalida el corpus en cada bump, y con edge rodante curl se actualiza sin que cambie una sola instruccion emitida. Quedan fuera a proposito, y no es una afirmacion de que no influyan: los autotools y las shells pueden cambiar ficheros generados. Es una decision de coste. Si algun dia se ve una divergencia que rastree ahi, se anaden — y ese dia el corpus se re-hashea otra vez. DE DONDE SALE: del apk db del rootfs REAL (cfg.rootfs, que respeta HAMMER_LAB/HAMMER_ROOTFS), no de docs/state/lab-toolchain.lock. El lock sigue siendo el registro legible que viaja por git; hashearlo permitiria sellar con un lab distinto del declarado. Derivar la ruta del padre del store se descarto: esa suposicion ya rompio al worker cuando su store se anclo a un volumen (ver defaults_for_store_with_lab). SIN CAMINO SILENCIOSO: el parametro es obligatorio, no Option. Sin rootfs falla y dice que hacer. Un default aqui reintroduciria la divergencia que esto cierra. Trae test de regresion de un fallo MUDO: la primera lista de prefijos llevaba el guion de version (`gcc-`) y en el apk db el campo P: es solo el nombre (`gcc`) ⇒ no casaba ninguno y la huella salia la del conjunto vacio. Un hash valido, constante e inutil, que mirando el hash no se nota. COSTE, medido y no estimado: sealed 768 -> 0, debt 777. Los 1745 artefactos del respaldo quedan SUPERADOS, no perdidos. Ninguna imagen queda lista. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2360,7 +2360,7 @@ open(os.path.join(os.path.dirname(out),"..","..","bins-seen.txt"),"w").write("\n
|
|||||||
let path = recipes_dir.join(format!("{name}.toml"));
|
let path = recipes_dir.join(format!("{name}.toml"));
|
||||||
let r = Recipe::load_from_path(&path).unwrap_or_else(|e| panic!("parse {name}: {e}"));
|
let r = Recipe::load_from_path(&path).unwrap_or_else(|e| panic!("parse {name}: {e}"));
|
||||||
assert_eq!(&r.name, name);
|
assert_eq!(&r.name, name);
|
||||||
let h = hammer_build::artifact_hash(&r, &store).expect("hash");
|
let h = hammer_build::artifact_hash(&r, &store, &hammer_core::lab::LabFingerprint::for_tests()).expect("hash");
|
||||||
assert!(h.as_str().starts_with("b3:"));
|
assert!(h.as_str().starts_with("b3:"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2375,7 +2375,7 @@ open(os.path.join(os.path.dirname(out),"..","..","bins-seen.txt"),"w").write("\n
|
|||||||
let path = recipes_dir.join(format!("{name}.toml"));
|
let path = recipes_dir.join(format!("{name}.toml"));
|
||||||
let r = Recipe::load_from_path(&path).unwrap_or_else(|e| panic!("parse {name}: {e}"));
|
let r = Recipe::load_from_path(&path).unwrap_or_else(|e| panic!("parse {name}: {e}"));
|
||||||
assert_eq!(&r.name, name);
|
assert_eq!(&r.name, name);
|
||||||
let h = hammer_build::artifact_hash(&r, &store).expect("hash");
|
let h = hammer_build::artifact_hash(&r, &store, &hammer_core::lab::LabFingerprint::for_tests()).expect("hash");
|
||||||
assert!(h.as_str().starts_with("b3:"), "hash con prefijo b3:");
|
assert!(h.as_str().starts_with("b3:"), "hash con prefijo b3:");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2696,7 +2696,7 @@ open(os.path.join(os.path.dirname(out),"..","..","bins-seen.txt"),"w").write("\n
|
|||||||
}
|
}
|
||||||
let r = Recipe::load_from_path(&path)
|
let r = Recipe::load_from_path(&path)
|
||||||
.unwrap_or_else(|e| panic!("parse {}: {e}", path.display()));
|
.unwrap_or_else(|e| panic!("parse {}: {e}", path.display()));
|
||||||
let h = hammer_build::artifact_hash(&r, &store)
|
let h = hammer_build::artifact_hash(&r, &store, &hammer_core::lab::LabFingerprint::for_tests())
|
||||||
.unwrap_or_else(|e| panic!("hash {}: {e}", path.display()));
|
.unwrap_or_else(|e| panic!("hash {}: {e}", path.display()));
|
||||||
assert!(h.as_str().starts_with("b3:"), "{}: hash con prefijo b3:", path.display());
|
assert!(h.as_str().starts_with("b3:"), "{}: hash con prefijo b3:", path.display());
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|||||||
@@ -30,7 +30,11 @@ pub use swm_bridge::build_source_patch;
|
|||||||
/// Errores: una receta con `deps.build` pero sin `base_dir` (parseada de TOML crudo, no cargada
|
/// Errores: una receta con `deps.build` pero sin `base_dir` (parseada de TOML crudo, no cargada
|
||||||
/// de disco) no puede resolver su catálogo; un `<dep>.toml` ausente, o un ciclo de deps, también
|
/// de disco) no puede resolver su catálogo; un `<dep>.toml` ausente, o un ciclo de deps, también
|
||||||
/// fallan con un mensaje que nombra la cadena.
|
/// fallan con un mensaje que nombra la cadena.
|
||||||
pub fn artifact_hash(recipe: &Recipe, _store: &Store) -> hammer_core::Result<ArtifactHash> {
|
pub fn artifact_hash(
|
||||||
|
recipe: &Recipe,
|
||||||
|
_store: &Store,
|
||||||
|
lab: &hammer_core::lab::LabFingerprint,
|
||||||
|
) -> hammer_core::Result<ArtifactHash> {
|
||||||
let mut stack: Vec<String> = Vec::new();
|
let mut stack: Vec<String> = Vec::new();
|
||||||
// Memo por path canónico de receta: el grafo de build es un DAG-diamante (p.ej. TODO KF6/Qt
|
// Memo por path canónico de receta: el grafo de build es un DAG-diamante (p.ej. TODO KF6/Qt
|
||||||
// converge en qtbase/kcoreaddons, alcanzables por decenas de rutas). Sin memoizar, el walk
|
// converge en qtbase/kcoreaddons, alcanzables por decenas de rutas). Sin memoizar, el walk
|
||||||
@@ -39,13 +43,14 @@ pub fn artifact_hash(recipe: &Recipe, _store: &Store) -> hammer_core::Result<Art
|
|||||||
// Clave = path resuelto (no `name`): dos variantes homónimas (incoming-kde/libxml2 vs canónica)
|
// Clave = path resuelto (no `name`): dos variantes homónimas (incoming-kde/libxml2 vs canónica)
|
||||||
// no deben colisionar. Ver granja-promote-colisiones.
|
// no deben colisionar. Ver granja-promote-colisiones.
|
||||||
let mut memo: HashMap<PathBuf, ArtifactHash> = HashMap::new();
|
let mut memo: HashMap<PathBuf, ArtifactHash> = HashMap::new();
|
||||||
artifact_hash_rec(recipe, &mut stack, &mut memo)
|
artifact_hash_rec(recipe, &mut stack, &mut memo, lab)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn artifact_hash_rec(
|
fn artifact_hash_rec(
|
||||||
recipe: &Recipe,
|
recipe: &Recipe,
|
||||||
stack: &mut Vec<String>,
|
stack: &mut Vec<String>,
|
||||||
memo: &mut HashMap<PathBuf, ArtifactHash>,
|
memo: &mut HashMap<PathBuf, ArtifactHash>,
|
||||||
|
lab: &hammer_core::lab::LabFingerprint,
|
||||||
) -> hammer_core::Result<ArtifactHash> {
|
) -> hammer_core::Result<ArtifactHash> {
|
||||||
if stack.iter().any(|n| n == &recipe.name) {
|
if stack.iter().any(|n| n == &recipe.name) {
|
||||||
stack.push(recipe.name.clone());
|
stack.push(recipe.name.clone());
|
||||||
@@ -55,8 +60,8 @@ fn artifact_hash_rec(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let dep_hashes = resolve_build_dep_hashes(recipe, stack, memo)?;
|
let dep_hashes = resolve_build_dep_hashes(recipe, stack, memo, lab)?;
|
||||||
let inputs = recipe.hash_inputs(&dep_hashes)?;
|
let inputs = recipe.hash_inputs(&dep_hashes, lab)?;
|
||||||
let refs: Vec<&[u8]> = inputs.iter().map(|v| v.as_slice()).collect();
|
let refs: Vec<&[u8]> = inputs.iter().map(|v| v.as_slice()).collect();
|
||||||
Ok(ArtifactHash::of_inputs(&refs))
|
Ok(ArtifactHash::of_inputs(&refs))
|
||||||
}
|
}
|
||||||
@@ -87,6 +92,7 @@ fn resolve_build_dep_hashes(
|
|||||||
recipe: &Recipe,
|
recipe: &Recipe,
|
||||||
stack: &mut Vec<String>,
|
stack: &mut Vec<String>,
|
||||||
memo: &mut HashMap<PathBuf, ArtifactHash>,
|
memo: &mut HashMap<PathBuf, ArtifactHash>,
|
||||||
|
lab: &hammer_core::lab::LabFingerprint,
|
||||||
) -> hammer_core::Result<Vec<ArtifactHash>> {
|
) -> hammer_core::Result<Vec<ArtifactHash>> {
|
||||||
if recipe.deps.build.is_empty() {
|
if recipe.deps.build.is_empty() {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
@@ -116,7 +122,7 @@ fn resolve_build_dep_hashes(
|
|||||||
dep_path.display()
|
dep_path.display()
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
let h = artifact_hash_rec(&dep_recipe, stack, memo)?;
|
let h = artifact_hash_rec(&dep_recipe, stack, memo, lab)?;
|
||||||
memo.insert(key, h.clone());
|
memo.insert(key, h.clone());
|
||||||
hashes.push(h);
|
hashes.push(h);
|
||||||
}
|
}
|
||||||
@@ -187,7 +193,13 @@ pub fn build(
|
|||||||
cfg: &BuildConfig,
|
cfg: &BuildConfig,
|
||||||
store: &Store,
|
store: &Store,
|
||||||
) -> hammer_core::Result<ArtifactHash> {
|
) -> hammer_core::Result<ArtifactHash> {
|
||||||
let h = artifact_hash(recipe, store)?;
|
// La huella sale del rootfs de ESTE cfg, que es el lab que de verdad va a compilar — no de
|
||||||
|
// `docs/state/lab-toolchain.lock` ni de una ruta derivada del store. Derivarla del padre del
|
||||||
|
// store ya mordió una vez (ver `BuildConfig::defaults_for_store_with_lab`): al anclar el store
|
||||||
|
// de un worker a un volumen, el lab se buscaba donde no estaba. `cfg.rootfs` respeta
|
||||||
|
// `HAMMER_LAB`/`HAMMER_ROOTFS` y es la única fuente que no puede desincronizarse del build.
|
||||||
|
let lab = hammer_core::lab::LabFingerprint::from_rootfs(&cfg.rootfs)?;
|
||||||
|
let h = artifact_hash(recipe, store, &lab)?;
|
||||||
if store.has(&h, &recipe.name) {
|
if store.has(&h, &recipe.name) {
|
||||||
tracing::info!(hash = %h, name = %recipe.name, "caché: artefacto ya en el store");
|
tracing::info!(hash = %h, name = %recipe.name, "caché: artefacto ya en el store");
|
||||||
return Ok(h);
|
return Ok(h);
|
||||||
@@ -1405,8 +1417,8 @@ mod dep_hash_tests {
|
|||||||
write_recipe(d.path(), "app", "a0", &["base"]);
|
write_recipe(d.path(), "app", "a0", &["base"]);
|
||||||
write_recipe(d.path(), "app_nodep", "a0", &[]); // misma fuente, sin la dep
|
write_recipe(d.path(), "app_nodep", "a0", &[]); // misma fuente, sin la dep
|
||||||
|
|
||||||
let with_dep = artifact_hash(&load(d.path(), "app"), &store).unwrap();
|
let with_dep = artifact_hash(&load(d.path(), "app"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap();
|
||||||
let no_dep = artifact_hash(&load(d.path(), "app_nodep"), &store).unwrap();
|
let no_dep = artifact_hash(&load(d.path(), "app_nodep"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap();
|
||||||
// `app_nodep` se llama distinto, pero el nombre no entra al hash: la única diferencia
|
// `app_nodep` se llama distinto, pero el nombre no entra al hash: la única diferencia
|
||||||
// de entrada es la presencia de la dep. (Renombramos sólo para tener dos .toml.)
|
// de entrada es la presencia de la dep. (Renombramos sólo para tener dos .toml.)
|
||||||
assert_ne!(with_dep, no_dep, "la dep de build debe entrar al hash");
|
assert_ne!(with_dep, no_dep, "la dep de build debe entrar al hash");
|
||||||
@@ -1419,11 +1431,11 @@ mod dep_hash_tests {
|
|||||||
write_recipe(d.path(), "base", "b0", &[]);
|
write_recipe(d.path(), "base", "b0", &[]);
|
||||||
write_recipe(d.path(), "lib", "l0", &["base"]);
|
write_recipe(d.path(), "lib", "l0", &["base"]);
|
||||||
write_recipe(d.path(), "app", "a0", &["lib"]);
|
write_recipe(d.path(), "app", "a0", &["lib"]);
|
||||||
let before = artifact_hash(&load(d.path(), "app"), &store).unwrap();
|
let before = artifact_hash(&load(d.path(), "app"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap();
|
||||||
|
|
||||||
// Cambiar la fuente de la dep transitiva `base` debe re-hashear `app`.
|
// Cambiar la fuente de la dep transitiva `base` debe re-hashear `app`.
|
||||||
write_recipe(d.path(), "base", "b1", &[]);
|
write_recipe(d.path(), "base", "b1", &[]);
|
||||||
let after = artifact_hash(&load(d.path(), "app"), &store).unwrap();
|
let after = artifact_hash(&load(d.path(), "app"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap();
|
||||||
assert_ne!(before, after, "un cambio transitivo debe propagarse");
|
assert_ne!(before, after, "un cambio transitivo debe propagarse");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1432,7 +1444,7 @@ mod dep_hash_tests {
|
|||||||
let d = tempfile::tempdir().unwrap();
|
let d = tempfile::tempdir().unwrap();
|
||||||
let store = store_in(d.path());
|
let store = store_in(d.path());
|
||||||
write_recipe(d.path(), "app", "a0", &["pcre2"]); // sin pcre2.toml
|
write_recipe(d.path(), "app", "a0", &["pcre2"]); // sin pcre2.toml
|
||||||
let err = artifact_hash(&load(d.path(), "app"), &store).unwrap_err().to_string();
|
let err = artifact_hash(&load(d.path(), "app"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap_err().to_string();
|
||||||
assert!(err.contains("pcre2"), "el error debe nombrar la dep: {err}");
|
assert!(err.contains("pcre2"), "el error debe nombrar la dep: {err}");
|
||||||
assert!(err.contains("app"), "y la receta que la pide: {err}");
|
assert!(err.contains("app"), "y la receta que la pide: {err}");
|
||||||
}
|
}
|
||||||
@@ -1443,7 +1455,7 @@ mod dep_hash_tests {
|
|||||||
let store = store_in(d.path());
|
let store = store_in(d.path());
|
||||||
write_recipe(d.path(), "a", "a0", &["b"]);
|
write_recipe(d.path(), "a", "a0", &["b"]);
|
||||||
write_recipe(d.path(), "b", "b0", &["a"]);
|
write_recipe(d.path(), "b", "b0", &["a"]);
|
||||||
let err = artifact_hash(&load(d.path(), "a"), &store).unwrap_err().to_string();
|
let err = artifact_hash(&load(d.path(), "a"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap_err().to_string();
|
||||||
assert!(err.contains("ciclo"), "esperaba ciclo: {err}");
|
assert!(err.contains("ciclo"), "esperaba ciclo: {err}");
|
||||||
assert!(err.contains("a -> b -> a"), "debe mostrar la cadena: {err}");
|
assert!(err.contains("a -> b -> a"), "debe mostrar la cadena: {err}");
|
||||||
}
|
}
|
||||||
@@ -1456,7 +1468,7 @@ mod dep_hash_tests {
|
|||||||
"name=\"x\"\nversion=\"1\"\n[source]\nrepo=\"git://x\"\ncommit=\"c0\"\n[build]\n[deps]\nbuild=[\"y\"]\n",
|
"name=\"x\"\nversion=\"1\"\n[source]\nrepo=\"git://x\"\ncommit=\"c0\"\n[build]\n[deps]\nbuild=[\"y\"]\n",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let err = artifact_hash(&r, &store).unwrap_err().to_string();
|
let err = artifact_hash(&r, &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap_err().to_string();
|
||||||
assert!(err.contains("base_dir"), "debe explicar la falta de base_dir: {err}");
|
assert!(err.contains("base_dir"), "debe explicar la falta de base_dir: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1465,8 +1477,8 @@ mod dep_hash_tests {
|
|||||||
let d = tempfile::tempdir().unwrap();
|
let d = tempfile::tempdir().unwrap();
|
||||||
let store = store_in(d.path());
|
let store = store_in(d.path());
|
||||||
write_recipe(d.path(), "solo", "s0", &[]);
|
write_recipe(d.path(), "solo", "s0", &[]);
|
||||||
let a = artifact_hash(&load(d.path(), "solo"), &store).unwrap();
|
let a = artifact_hash(&load(d.path(), "solo"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap();
|
||||||
let b = artifact_hash(&load(d.path(), "solo"), &store).unwrap();
|
let b = artifact_hash(&load(d.path(), "solo"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap();
|
||||||
assert_eq!(a, b, "sin deps el hash es estable");
|
assert_eq!(a, b, "sin deps el hash es estable");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -945,7 +945,12 @@ fn plan(
|
|||||||
let derived = hammer_core::kernel::plan::derive_recipe(&base, &p);
|
let derived = hammer_core::kernel::plan::derive_recipe(&base, &p);
|
||||||
match hammer_core::Store::open(Path::new(store))
|
match hammer_core::Store::open(Path::new(store))
|
||||||
.map_err(anyhow::Error::from)
|
.map_err(anyhow::Error::from)
|
||||||
.and_then(|s| hammer_build::artifact_hash(&derived, &s).map_err(anyhow::Error::from))
|
.and_then(|s| {
|
||||||
|
let cfg = hammer_build::BuildConfig::from_env_or_defaults(s.root());
|
||||||
|
let lab = hammer_core::lab::LabFingerprint::from_rootfs(&cfg.rootfs)
|
||||||
|
.map_err(anyhow::Error::from)?;
|
||||||
|
hammer_build::artifact_hash(&derived, &s, &lab).map_err(anyhow::Error::from)
|
||||||
|
})
|
||||||
{
|
{
|
||||||
Ok(h) => p.artifact_hash = Some(h.to_string()),
|
Ok(h) => p.artifact_hash = Some(h.to_string()),
|
||||||
Err(e) => eprintln!("aviso: no pude calcular el ArtifactHash ({e})"),
|
Err(e) => eprintln!("aviso: no pude calcular el ArtifactHash ({e})"),
|
||||||
|
|||||||
@@ -792,7 +792,11 @@ fn main() -> anyhow::Result<()> {
|
|||||||
Cmd::Hash { recipe, check } => {
|
Cmd::Hash { recipe, check } => {
|
||||||
let store = hammer_core::Store::open(&cli.store)?;
|
let store = hammer_core::Store::open(&cli.store)?;
|
||||||
let recipe = hammer_core::Recipe::load_from_path(&recipe)?;
|
let recipe = hammer_core::Recipe::load_from_path(&recipe)?;
|
||||||
let hash = hammer_build::artifact_hash(&recipe, &store)?;
|
// Misma resolución de lab que `build`, o `hash` mentiría: el toolchain entra en el
|
||||||
|
// ArtifactHash, así que un `hash` que no lo mire daría una dirección que `build` no usa.
|
||||||
|
let cfg = hammer_build::BuildConfig::from_env_or_defaults(store.root());
|
||||||
|
let lab = hammer_core::lab::LabFingerprint::from_rootfs(&cfg.rootfs)?;
|
||||||
|
let hash = hammer_build::artifact_hash(&recipe, &store, &lab)?;
|
||||||
if check {
|
if check {
|
||||||
// ¿el artefacto VIGENTE (el que corresponde a la receta de HOY) ya está sellado?
|
// ¿el artefacto VIGENTE (el que corresponde a la receta de HOY) ya está sellado?
|
||||||
// Es lo que static-audit.sh no podía saber: `ls -dt store/*-<n>` da el más RECIENTE,
|
// Es lo que static-audit.sh no podía saber: `ls -dt store/*-<n>` da el más RECIENTE,
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
//! La huella del **lab**: qué toolchain construyó un artefacto.
|
||||||
|
//!
|
||||||
|
//! ── POR QUÉ EXISTE ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
//! Medido el 2026-08-10 reconstruyendo los cuatro kernels en un segundo hub: el `.config` que la
|
||||||
|
//! receta instala salió distinto del que produjo el build anterior, en **4 líneas y ninguna del
|
||||||
|
//! cambio de receta**:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! CONFIG_RUSTC_VERSION=109600 → 109700
|
||||||
|
//! CONFIG_RUSTC_LLVM_VERSION=220103 → 220108
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Rust 1.96 en una máquina, 1.97 en la otra — las dos resueltas del mismo `apk add` contra Alpine
|
||||||
|
//! edge, que es rodante. Y **Rust ni siquiera estaba activado en esos kernels**: Kconfig sondea el
|
||||||
|
//! `rustc` del entorno y graba su versión igual. Una herramienta que el artefacto no usa le cambia
|
||||||
|
//! los bytes.
|
||||||
|
//!
|
||||||
|
//! Antes de esto el toolchain del rootfs NO era una entrada del `ArtifactHash` ⇒ **dos labs sellaban
|
||||||
|
//! bytes distintos en la MISMA dirección**, y el store no tenía forma de notarlo: para él un
|
||||||
|
//! artefacto *es* su dirección. Un `mirror pull` entre dos hubs se llevaba cualquiera de los dos.
|
||||||
|
//!
|
||||||
|
//! ── QUÉ ENTRA Y QUÉ NO ──────────────────────────────────────────────────────────────────────────
|
||||||
|
//! NO entra el rootfs entero. El criterio es: **¿la versión de este paquete puede cambiar los BYTES
|
||||||
|
//! del artefacto?** Si no puede, no entra, porque cada paquete de más es una invalidación del corpus
|
||||||
|
//! entero cada vez que Alpine lo bumpee — y con edge rodante `curl` se actualiza seguido sin que
|
||||||
|
//! nada de lo que emite el compilador cambie.
|
||||||
|
//!
|
||||||
|
//! Entran, y por qué:
|
||||||
|
//! · compiladores y enlazadores: gcc, g++, clang*, llvm*, binutils, rust, cargo
|
||||||
|
//! · las libs con las que gcc hace CODEGEN: gmp, mpfr4, mpc1, isl* — un bump cambia lo que emite
|
||||||
|
//! · runtime que se ENLAZA dentro: musl*, libgcc*, libstdc++*, libatomic, libgomp
|
||||||
|
//! · headers que se COMPILAN dentro: linux-headers, fortify-headers
|
||||||
|
//!
|
||||||
|
//! Quedan fuera a propósito, aun sabiendo que no es una línea perfecta: los autotools
|
||||||
|
//! (`m4`/`autoconf`/`automake`/`libtool`/`make`/`pkgconf`) y las shells (`bash`/`busybox`/
|
||||||
|
//! `coreutils`). Pueden cambiar ficheros generados y por tanto el output, pero casi todas las
|
||||||
|
//! recetas traen su `configure` ya generado en el tarball y `autoreconf` es raro. **Es una decisión
|
||||||
|
//! de coste, no una afirmación de que no influyen**: si algún día se ve una divergencia que
|
||||||
|
//! rastree hasta ahí, se añaden — y ese día el corpus se re-hashea otra vez.
|
||||||
|
//!
|
||||||
|
//! ── LA VERDAD ESTÁ EN EL ROOTFS, NO EN UN FICHERO APARTE ────────────────────────────────────────
|
||||||
|
//! La huella se computa del `apk db` del rootfs REAL que se va a usar, no de
|
||||||
|
//! `docs/state/lab-toolchain.lock`. El lock es el registro legible y versionado (viaja por git, que
|
||||||
|
//! es como dos hubs se comparan) y `bootstrap-devfs.sh` avisa cuando divergen; pero hashear el lock
|
||||||
|
//! permitiría sellar con un lab distinto del declarado y quedarse tan tranquilo. Se hashea lo que
|
||||||
|
//! de verdad va a compilar.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Prefijos de paquete cuya VERSIÓN puede cambiar los bytes de un artefacto. Ver el módulo.
|
||||||
|
///
|
||||||
|
/// Son prefijos y no nombres exactos porque Alpine versiona en el nombre (`clang22`, `llvm22`,
|
||||||
|
/// `isl26`) y sub-paqueta (`clang22-libs`, `musl-dev`, `libstdc++-dev`). Un prefijo captura la
|
||||||
|
/// familia entera sin tener que listar cada variante ni actualizar esto en cada bump mayor.
|
||||||
|
///
|
||||||
|
/// ⚠ Se comparan contra el **NOMBRE** del paquete (`P:` del apk db), sin versión: son `gcc`, no
|
||||||
|
/// `gcc-`. El guion sólo existe en el formato `nombre-versión` que imprime `apk info -v` y guarda
|
||||||
|
/// el lock; escribirlo aquí hace que NADA case y la huella salga la del conjunto vacío — que es un
|
||||||
|
/// hash perfectamente válido y constante, o sea un fallo mudo. Lo cazó un test, no una revisión.
|
||||||
|
pub const TOOLCHAIN_PREFIXES: &[&str] = &[
|
||||||
|
"binutils",
|
||||||
|
"cargo",
|
||||||
|
"clang",
|
||||||
|
"fortify-headers",
|
||||||
|
"g++",
|
||||||
|
"gcc",
|
||||||
|
"gmp",
|
||||||
|
"isl",
|
||||||
|
"libatomic",
|
||||||
|
"libgcc",
|
||||||
|
"libgomp",
|
||||||
|
"libstdc++",
|
||||||
|
"linux-headers",
|
||||||
|
"llvm",
|
||||||
|
"mpc1",
|
||||||
|
"mpfr4",
|
||||||
|
"musl",
|
||||||
|
"rust",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Huella del toolchain del lab. Opaca a propósito: sólo se compara y se mete al hash.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LabFingerprint(String);
|
||||||
|
|
||||||
|
impl LabFingerprint {
|
||||||
|
/// Huella fija para tests y para árboles herméticos sin rootfs.
|
||||||
|
///
|
||||||
|
/// Existe para que los tests no necesiten un lab de verdad **sin abrir un camino silencioso**:
|
||||||
|
/// no hay `Option` ni default que se aplique solo. Quien no tiene lab lo dice explícitamente.
|
||||||
|
pub fn for_tests() -> Self {
|
||||||
|
Self("test".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lee el `apk db` de un rootfs y resume los paquetes de [`TOOLCHAIN_PREFIXES`].
|
||||||
|
///
|
||||||
|
/// Falla si el db no existe. **No cae a un default**: un hash calculado sin lab y uno calculado
|
||||||
|
/// con lab son direcciones distintas para el mismo contenido, así que adivinar aquí produciría
|
||||||
|
/// exactamente la divergencia silenciosa que este módulo existe para cerrar.
|
||||||
|
pub fn from_rootfs(rootfs: &Path) -> crate::Result<Self> {
|
||||||
|
let db = rootfs.join("lib/apk/db/installed");
|
||||||
|
let texto = std::fs::read_to_string(&db).map_err(|e| {
|
||||||
|
crate::Error::Other(anyhow::anyhow!(
|
||||||
|
"no pude leer el apk db del lab en {}: {e}. \
|
||||||
|
El toolchain entra en el ArtifactHash, así que sin rootfs no se puede calcular \
|
||||||
|
un hash comparable (corré scripts/bootstrap-devfs.sh, o apuntá HAMMER_ROOTFS)",
|
||||||
|
db.display()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Ok(Self::from_apk_db(&texto))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Igual que [`Self::from_rootfs`] pero sobre el texto del db ya leído (testeable sin ficheros).
|
||||||
|
///
|
||||||
|
/// El formato de `lib/apk/db/installed` son bloques `P:<nombre>` … `V:<versión>` separados por
|
||||||
|
/// líneas en blanco. Se reconstruye `<nombre>-<versión>`, que es la misma forma que imprime
|
||||||
|
/// `apk info -v` y la que guarda `docs/state/lab-toolchain.lock` ⇒ los dos se leen igual.
|
||||||
|
pub fn from_apk_db(texto: &str) -> Self {
|
||||||
|
let mut nombre: Option<&str> = None;
|
||||||
|
let mut paquetes: Vec<String> = Vec::new();
|
||||||
|
for linea in texto.lines() {
|
||||||
|
if let Some(n) = linea.strip_prefix("P:") {
|
||||||
|
nombre = Some(n);
|
||||||
|
} else if let Some(v) = linea.strip_prefix("V:") {
|
||||||
|
if let Some(n) = nombre.take() {
|
||||||
|
if TOOLCHAIN_PREFIXES.iter().any(|p| n.starts_with(p)) {
|
||||||
|
paquetes.push(format!("{n}-{v}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ordenar: el orden del db depende del orden de instalación, que no es una propiedad del
|
||||||
|
// toolchain. Sin esto, dos rootfs idénticos instalados en distinto orden darían huellas
|
||||||
|
// distintas y re-hashearían el corpus por nada.
|
||||||
|
paquetes.sort();
|
||||||
|
paquetes.dedup();
|
||||||
|
let mut h = blake3::Hasher::new();
|
||||||
|
for p in &paquetes {
|
||||||
|
h.update(p.as_bytes());
|
||||||
|
h.update(b"\n");
|
||||||
|
}
|
||||||
|
Self(h.finalize().to_hex().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const DB: &str = "\
|
||||||
|
P:gcc
|
||||||
|
V:15.2.0-r8
|
||||||
|
|
||||||
|
P:curl
|
||||||
|
V:8.21.0-r0
|
||||||
|
|
||||||
|
P:rust
|
||||||
|
V:1.97.0-r0
|
||||||
|
";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn la_huella_no_es_la_del_conjunto_vacio() {
|
||||||
|
// REGRESIÓN: la primera versión comparaba prefijos con guion (`gcc-`) contra el NOMBRE
|
||||||
|
// (`gcc`) ⇒ no casaba ninguno y la huella era la del conjunto vacío: constante, válida y
|
||||||
|
// completamente inútil. Un filtro que no selecciona nada no se nota mirando el hash.
|
||||||
|
let vacia = LabFingerprint::from_apk_db("");
|
||||||
|
assert_ne!(
|
||||||
|
LabFingerprint::from_apk_db(DB),
|
||||||
|
vacia,
|
||||||
|
"si esto falla, el filtro no está seleccionando NADA"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filtra_solo_el_toolchain() {
|
||||||
|
let a = LabFingerprint::from_apk_db(DB);
|
||||||
|
// Cambiar curl NO mueve la huella: no puede cambiar los bytes de un artefacto, y si contara
|
||||||
|
// el corpus entero se invalidaría en cada bump de Alpine edge.
|
||||||
|
let b = LabFingerprint::from_apk_db(&DB.replace("8.21.0-r0", "8.22.0-r0"));
|
||||||
|
assert_eq!(a, b, "curl no es toolchain");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn el_compilador_si_mueve_la_huella() {
|
||||||
|
let a = LabFingerprint::from_apk_db(DB);
|
||||||
|
// El caso REAL medido el 2026-08-10: rust 1.96 vs 1.97 entre dos hubs.
|
||||||
|
let b = LabFingerprint::from_apk_db(&DB.replace("1.97.0-r0", "1.96.0-r0"));
|
||||||
|
assert_ne!(a, b, "un rustc distinto es un lab distinto");
|
||||||
|
let c = LabFingerprint::from_apk_db(&DB.replace("15.2.0-r8", "15.3.0-r0"));
|
||||||
|
assert_ne!(a, c, "un gcc distinto es un lab distinto");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn el_orden_de_instalacion_no_cuenta() {
|
||||||
|
let al_reves = "P:rust\nV:1.97.0-r0\n\nP:gcc\nV:15.2.0-r8\n";
|
||||||
|
assert_eq!(
|
||||||
|
LabFingerprint::from_apk_db(DB).as_str(),
|
||||||
|
LabFingerprint::from_apk_db(al_reves).as_str(),
|
||||||
|
"el orden del db es del instalador, no del toolchain"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rootfs_ausente_falla_en_vez_de_adivinar() {
|
||||||
|
let err = LabFingerprint::from_rootfs(Path::new("/no/existe")).unwrap_err();
|
||||||
|
assert!(
|
||||||
|
err.to_string().contains("apk db del lab"),
|
||||||
|
"el error debe decir qué falta: {err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ pub mod differs;
|
|||||||
pub mod hash;
|
pub mod hash;
|
||||||
pub mod installed;
|
pub mod installed;
|
||||||
pub mod kernel;
|
pub mod kernel;
|
||||||
|
pub mod lab;
|
||||||
pub mod proto;
|
pub mod proto;
|
||||||
pub mod query;
|
pub mod query;
|
||||||
pub mod recipe;
|
pub mod recipe;
|
||||||
|
|||||||
@@ -482,9 +482,16 @@ impl Recipe {
|
|||||||
/// Las entradas canónicas que alimentan el `ArtifactHash` de esta receta. Incluye el
|
/// Las entradas canónicas que alimentan el `ArtifactHash` de esta receta. Incluye el
|
||||||
/// **contenido** de cada patch (no su ruta), de modo que renombrar un archivo no cambia el
|
/// **contenido** de cada patch (no su ruta), de modo que renombrar un archivo no cambia el
|
||||||
/// hash y editarlo sí lo hace. Ver `docs/02-build-lab.md` §2.
|
/// hash y editarlo sí lo hace. Ver `docs/02-build-lab.md` §2.
|
||||||
|
/// `lab` es la huella del toolchain que va a construir esto (ver [`crate::lab`]). Entra SIEMPRE
|
||||||
|
/// y es un parámetro obligatorio, no un `Option`: el 2026-08-10 se midió que dos labs con
|
||||||
|
/// distinto `rustc` producen bytes distintos, así que un hash calculado sin lab y otro con lab
|
||||||
|
/// son direcciones distintas para el mismo contenido. Un default silencioso aquí reintroduciría
|
||||||
|
/// exactamente la divergencia que esto cierra — quien no tiene lab usa
|
||||||
|
/// [`crate::lab::LabFingerprint::for_tests`] y lo dice.
|
||||||
pub fn hash_inputs(
|
pub fn hash_inputs(
|
||||||
&self,
|
&self,
|
||||||
dep_hashes: &[ArtifactHash],
|
dep_hashes: &[ArtifactHash],
|
||||||
|
lab: &crate::lab::LabFingerprint,
|
||||||
) -> crate::Result<Vec<Vec<u8>>> {
|
) -> crate::Result<Vec<Vec<u8>>> {
|
||||||
// El identificador inmutable del contenido fuente: el commit en modo git, el
|
// El identificador inmutable del contenido fuente: el commit en modo git, el
|
||||||
// sha256 del archivo en modo tarball. Cualquiera de los dos es un puntero al
|
// sha256 del archivo en modo tarball. Cualquiera de los dos es un puntero al
|
||||||
@@ -498,6 +505,7 @@ impl Recipe {
|
|||||||
self.build.compiler.as_str().as_bytes().to_vec(),
|
self.build.compiler.as_str().as_bytes().to_vec(),
|
||||||
self.build.target.as_bytes().to_vec(),
|
self.build.target.as_bytes().to_vec(),
|
||||||
self.build.link.as_str().as_bytes().to_vec(),
|
self.build.link.as_str().as_bytes().to_vec(),
|
||||||
|
format!("lab:{}", lab.as_str()).into_bytes(),
|
||||||
];
|
];
|
||||||
// Sólo entra al hash si está fijado: una receta sin `zig_version` mantiene su hash de antes
|
// Sólo entra al hash si está fijado: una receta sin `zig_version` mantiene su hash de antes
|
||||||
// de existir el campo (compatibilidad hacia atrás; el baseline 9adefb82 no se mueve).
|
// de existir el campo (compatibilidad hacia atrás; el baseline 9adefb82 no se mueve).
|
||||||
@@ -651,7 +659,7 @@ sha256 = "1f31014953e71c3cddcedb97692ad7620cb9d6d04fbdc19e0d8dd836f87622bb"
|
|||||||
flags = ["--enable-perl-regexp"]
|
flags = ["--enable-perl-regexp"]
|
||||||
"#;
|
"#;
|
||||||
let tar = Recipe::from_toml(tar_toml).unwrap();
|
let tar = Recipe::from_toml(tar_toml).unwrap();
|
||||||
assert_ne!(git.hash_inputs(&[]).unwrap(), tar.hash_inputs(&[]).unwrap());
|
assert_ne!(git.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap(), tar.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -674,8 +682,8 @@ commit = "deadbeef"
|
|||||||
#[test]
|
#[test]
|
||||||
fn hash_no_patches_is_deterministic() {
|
fn hash_no_patches_is_deterministic() {
|
||||||
let r = Recipe::from_toml(SAMPLE).unwrap();
|
let r = Recipe::from_toml(SAMPLE).unwrap();
|
||||||
let a = r.hash_inputs(&[]).unwrap();
|
let a = r.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap();
|
||||||
let b = r.hash_inputs(&[]).unwrap();
|
let b = r.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap();
|
||||||
assert_eq!(a, b);
|
assert_eq!(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,7 +703,7 @@ commit = "deadbeef"
|
|||||||
r2.base_dir = dir2.path().to_path_buf();
|
r2.base_dir = dir2.path().to_path_buf();
|
||||||
|
|
||||||
// Mismo CONTENIDO ⇒ misma entrada al hash, aunque las rutas sean distintas.
|
// Mismo CONTENIDO ⇒ misma entrada al hash, aunque las rutas sean distintas.
|
||||||
assert_eq!(r1.hash_inputs(&[]).unwrap(), r2.hash_inputs(&[]).unwrap());
|
assert_eq!(r1.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap(), r2.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -707,10 +715,10 @@ commit = "deadbeef"
|
|||||||
let mut r = Recipe::from_toml(SAMPLE).unwrap();
|
let mut r = Recipe::from_toml(SAMPLE).unwrap();
|
||||||
r.source.patches = vec!["p.patch".into()];
|
r.source.patches = vec!["p.patch".into()];
|
||||||
r.base_dir = dir.path().to_path_buf();
|
r.base_dir = dir.path().to_path_buf();
|
||||||
let h_a = r.hash_inputs(&[]).unwrap();
|
let h_a = r.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap();
|
||||||
|
|
||||||
std::fs::write(&patch, b"version B").unwrap();
|
std::fs::write(&patch, b"version B").unwrap();
|
||||||
let h_b = r.hash_inputs(&[]).unwrap();
|
let h_b = r.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap();
|
||||||
assert_ne!(h_a, h_b);
|
assert_ne!(h_a, h_b);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -719,7 +727,7 @@ commit = "deadbeef"
|
|||||||
let mut r = Recipe::from_toml(SAMPLE).unwrap();
|
let mut r = Recipe::from_toml(SAMPLE).unwrap();
|
||||||
r.source.patches = vec!["no-existe.patch".into()];
|
r.source.patches = vec!["no-existe.patch".into()];
|
||||||
r.base_dir = PathBuf::from("/tmp/seguro-que-no-existe-hammer");
|
r.base_dir = PathBuf::from("/tmp/seguro-que-no-existe-hammer");
|
||||||
let err = r.hash_inputs(&[]).unwrap_err().to_string();
|
let err = r.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap_err().to_string();
|
||||||
assert!(err.contains("no-existe.patch"), "msg = {err}");
|
assert!(err.contains("no-existe.patch"), "msg = {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -788,8 +796,8 @@ expected_output = "b3:deadbeef"
|
|||||||
sin.evidence = Evidence::default();
|
sin.evidence = Evidence::default();
|
||||||
assert!(con.evidence.checks.len() == 3 && sin.evidence.is_empty());
|
assert!(con.evidence.checks.len() == 3 && sin.evidence.is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sin.hash_inputs(&[]).unwrap(),
|
sin.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap(),
|
||||||
con.hash_inputs(&[]).unwrap(),
|
con.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap(),
|
||||||
"la evidencia NO debe entrar en hash_inputs"
|
"la evidencia NO debe entrar en hash_inputs"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -804,8 +812,8 @@ expected_output = "b3:deadbeef"
|
|||||||
let mut con = sin.clone();
|
let mut con = sin.clone();
|
||||||
con.source.cargo_vendor = Some(true);
|
con.source.cargo_vendor = Some(true);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sin.hash_inputs(&[]).unwrap(),
|
sin.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap(),
|
||||||
con.hash_inputs(&[]).unwrap(),
|
con.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap(),
|
||||||
"cargo_vendor NO debe entrar en hash_inputs"
|
"cargo_vendor NO debe entrar en hash_inputs"
|
||||||
);
|
);
|
||||||
// Y roundtripea por TOML: si se perdiera al serializar, el forzado se apagaría solo en
|
// Y roundtripea por TOML: si se perdiera al serializar, el forzado se apagaría solo en
|
||||||
@@ -843,8 +851,8 @@ expected_output = "b3:deadbeef"
|
|||||||
sin.slots = Slots::default();
|
sin.slots = Slots::default();
|
||||||
assert!(!con.slots.is_empty() && sin.slots.is_empty());
|
assert!(!con.slots.is_empty() && sin.slots.is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sin.hash_inputs(&[]).unwrap(),
|
sin.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap(),
|
||||||
con.hash_inputs(&[]).unwrap(),
|
con.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap(),
|
||||||
"los slots NO deben entrar en hash_inputs"
|
"los slots NO deben entrar en hash_inputs"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -878,8 +886,8 @@ expected_output = "b3:deadbeef"
|
|||||||
|
|
||||||
// Y el hash es el MISMO con y sin licencia.
|
// Y el hash es el MISMO con y sin licencia.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sin.hash_inputs(&[]).unwrap(),
|
sin.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap(),
|
||||||
con.hash_inputs(&[]).unwrap(),
|
con.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap(),
|
||||||
"la licencia NO debe entrar en hash_inputs: si entrara, declararla re-hashearía las \
|
"la licencia NO debe entrar en hash_inputs: si entrara, declararla re-hashearía las \
|
||||||
1141 recetas ya selladas y tiraría el baseline de reproducibilidad"
|
1141 recetas ya selladas y tiraría el baseline de reproducibilidad"
|
||||||
);
|
);
|
||||||
@@ -903,8 +911,8 @@ expected_output = "b3:deadbeef"
|
|||||||
let mut con = Recipe::from_toml(SAMPLE).unwrap();
|
let mut con = Recipe::from_toml(SAMPLE).unwrap();
|
||||||
con.build.strip_debug = Some(true);
|
con.build.strip_debug = Some(true);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
sin.hash_inputs(&[]).unwrap(),
|
sin.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap(),
|
||||||
con.hash_inputs(&[]).unwrap(),
|
con.hash_inputs(&[], &crate::lab::LabFingerprint::for_tests()).unwrap(),
|
||||||
"strip_debug DEBE entrar en hash_inputs: cambia el contenido del artefacto"
|
"strip_debug DEBE entrar en hash_inputs: cambia el contenido del artefacto"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+3849
-2161
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user