hammer-bootstrap: userland Rust en el producto + adelgazar busybox (Etapa C)
Extiende la capa de producto con el userland Rust-nativo ADOPTADO, fuera del núcleo blindado (sigue sin tocar STAGE1_COMPONENTS ni el of_tree). - USERLAND_COMPONENTS = [uutils, findutils, findutils-xargs, diffutils, ripgrep]: se hidratan sobre el 4/4 DESPUÉS de busybox ⇒ sus symlinks en /usr/bin ensombrecen los applets busybox. - ADELGAZAR BUSYBOX (determinista, sin depender del PATH del shell): por cada nombre que el userland Rust provee en /usr/bin, se RETIRA el symlink homónimo de busybox en /bin, /sbin, /usr/sbin (busybox suele dejar `ls` en /bin, fuera del /usr/bin ya ensombrecido). La tool Rust queda ÚNICA en PATH. El binario busybox y lo no reemplazado (sh/ash, tar, mount) se preservan. - product_rootfs_hash v2 (incluye userland); build_components() helper; assemble_product_rootfs hidrata base→userland→servicios. - 36 tests verde (nuevo: ensombrecido + retiro de applet + sh/busybox quedan). Validado in-VM (product-boot-test.sh sobre el product-rootfs de la ruta real): ls = "uutils coreutils 0.9.0" (/usr/bin/ls), find = "find (Rust) 0.9.1", rg = "ripgrep 14.1.1"; /bin/ls y /bin/cat retirados, /bin/sh + busybox preservados. SSH sigue verde. "Adelgazar busybox" cerrado en el producto. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -396,9 +396,17 @@ fn assemble_rootfs(
|
|||||||
// Así, meter más servicios mañana no toca jamás el núcleo verificado.
|
// Así, meter más servicios mañana no toca jamás el núcleo verificado.
|
||||||
|
|
||||||
/// Componentes de servicio inyectados sobre el 4/4 verificado: `netup` (red) + `openssh` (sshd/ssh).
|
/// Componentes de servicio inyectados sobre el 4/4 verificado: `netup` (red) + `openssh` (sshd/ssh).
|
||||||
/// Crecer esta lista NO toca `STAGE1_COMPONENTS` ni el selfhost-verify.
|
/// Daemons ⇒ llevan card en la seed de producto. Crecer esta lista NO toca `STAGE1_COMPONENTS`.
|
||||||
const SERVICE_COMPONENTS: &[&str] = &["netup", "openssh"];
|
const SERVICE_COMPONENTS: &[&str] = &["netup", "openssh"];
|
||||||
|
|
||||||
|
/// Userland Rust-nativo ADOPTADO (Etapa C): multicall `uutils`/coreutils + `find`/`xargs` + `diff`/`cmp`
|
||||||
|
/// + `rg`. Se hidrata sobre la base DESPUÉS de busybox, así que sus symlinks en `/usr/bin` ENSOMBRECEN
|
||||||
|
/// los applets de busybox (con `PATH` `/usr/bin:/bin` gana la tool Rust). Es el "adelgazar busybox" del
|
||||||
|
/// producto SIN tocar el núcleo: busybox sigue en el 4/4 (self-host) y queda de fallback para lo aún no
|
||||||
|
/// reemplazado (`sh`/ash, `tar`, …). NO son daemons ⇒ no llevan card en la seed (sólo ficheros).
|
||||||
|
const USERLAND_COMPONENTS: &[&str] =
|
||||||
|
&["uutils", "findutils", "findutils-xargs", "diffutils", "ripgrep"];
|
||||||
|
|
||||||
/// Card de servicio `sshd` (genesis `Native`/`Restart`): el comando levanta la red (`netup`, DHCP),
|
/// Card de servicio `sshd` (genesis `Native`/`Restart`): el comando levanta la red (`netup`, DHCP),
|
||||||
/// genera host keys al primer boot (`ssh-keygen -A`) y `exec`uta `sshd -D`. Todo dentro del card para
|
/// genera host keys al primer boot (`ssh-keygen -A`) y `exec`uta `sshd -D`. Todo dentro del card para
|
||||||
/// no asumir orden de genesis. Mismo esquema validado de [`STAGE1_SEED_CARD`]; ULID propio. Se inyecta
|
/// no asumir orden de genesis. Mismo esquema validado de [`STAGE1_SEED_CARD`]; ULID propio. Se inyecta
|
||||||
@@ -454,17 +462,19 @@ fn product_seed_card() -> Result<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Identidad del `product-rootfs`: tag + el `RootfsHash` del 4/4 base + `(nombre,hash)` de cada
|
/// Identidad del `product-rootfs`: tag + el `RootfsHash` del 4/4 base + `(nombre,hash)` de cada
|
||||||
/// servicio + la seed de producto + las configs. Cambiar cualquiera re-hashea (reproducible).
|
/// componente userland y de servicio (en orden) + la seed de producto + las configs. Cambiar
|
||||||
|
/// cualquiera re-hashea (reproducible). El tag `v2` marca el corte respecto al producto sólo-servicios.
|
||||||
fn product_rootfs_hash(
|
fn product_rootfs_hash(
|
||||||
base: &RootfsHash,
|
base: &RootfsHash,
|
||||||
|
userland: &[(String, ArtifactHash)],
|
||||||
services: &[(String, ArtifactHash)],
|
services: &[(String, ArtifactHash)],
|
||||||
seed: &str,
|
seed: &str,
|
||||||
) -> RootfsHash {
|
) -> RootfsHash {
|
||||||
let mut inputs: Vec<Vec<u8>> = vec![
|
let mut inputs: Vec<Vec<u8>> = vec![
|
||||||
b"hammer-product-rootfs-v1".to_vec(),
|
b"hammer-product-rootfs-v2".to_vec(),
|
||||||
base.as_str().as_bytes().to_vec(),
|
base.as_str().as_bytes().to_vec(),
|
||||||
];
|
];
|
||||||
for (name, h) in services {
|
for (name, h) in userland.iter().chain(services.iter()) {
|
||||||
inputs.push(name.as_bytes().to_vec());
|
inputs.push(name.as_bytes().to_vec());
|
||||||
inputs.push(h.as_str().as_bytes().to_vec());
|
inputs.push(h.as_str().as_bytes().to_vec());
|
||||||
}
|
}
|
||||||
@@ -476,13 +486,14 @@ fn product_rootfs_hash(
|
|||||||
ArtifactHash::of_inputs(&refs)
|
ArtifactHash::of_inputs(&refs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensambla el árbol de producto en `staging`: hidrata el rootfs base sellado (4/4) y luego los
|
/// Ensambla el árbol de producto en `staging`: hidrata el rootfs base sellado (4/4), luego el userland
|
||||||
/// componentes de servicio ENCIMA (hidratación tardía); sobrescribe la seed con la de producto y
|
/// Rust (ENSOMBRECE los applets de busybox en `/usr/bin`) y los componentes de servicio ENCIMA
|
||||||
/// escribe las configs de servicio (passwd/group/sshd_config + `/var/empty` 0711, `/etc/ssh`,
|
/// (hidratación tardía); sobrescribe la seed con la de producto y escribe las configs de servicio
|
||||||
/// `/root/.ssh` 0700). Pieza testeable sin build real.
|
/// (passwd/group/sshd_config + `/var/empty` 0711, `/etc/ssh`, `/root/.ssh` 0700). Testeable sin build.
|
||||||
fn assemble_product_rootfs(
|
fn assemble_product_rootfs(
|
||||||
store: &Store,
|
store: &Store,
|
||||||
base: &RootfsHash,
|
base: &RootfsHash,
|
||||||
|
userland: &[(String, ArtifactHash)],
|
||||||
services: &[(String, ArtifactHash)],
|
services: &[(String, ArtifactHash)],
|
||||||
seed: &str,
|
seed: &str,
|
||||||
staging: &Path,
|
staging: &Path,
|
||||||
@@ -500,13 +511,38 @@ fn assemble_product_rootfs(
|
|||||||
}
|
}
|
||||||
hammer_build::run_hydrate(&base_dir, staging, hammer_core::LinkMode::Static, None)?;
|
hammer_build::run_hydrate(&base_dir, staging, hammer_core::LinkMode::Static, None)?;
|
||||||
|
|
||||||
// 2) servicios encima (openssh, netup): mismo proyectado por hardlink.
|
// 2) userland Rust + servicios ENCIMA (hardlink). El userland va DESPUÉS de la base: sus symlinks
|
||||||
for (name, h) in services {
|
// en /usr/bin reemplazan a los de busybox (run_hydrate sustituye el symlink existente) ⇒ el
|
||||||
|
// producto corre las tools Rust. Orden userland→servicios (sin colisiones entre sí).
|
||||||
|
for (name, h) in userland.iter().chain(services.iter()) {
|
||||||
let dir = store.path_of(h, name);
|
let dir = store.path_of(h, name);
|
||||||
hammer_build::run_hydrate(&dir, staging, hammer_core::LinkMode::Static, None)?;
|
hammer_build::run_hydrate(&dir, staging, hammer_core::LinkMode::Static, None)?;
|
||||||
}
|
}
|
||||||
let _ = std::fs::remove_dir_all(staging.join(".hammer"));
|
let _ = std::fs::remove_dir_all(staging.join(".hammer"));
|
||||||
|
|
||||||
|
// ADELGAZAR BUSYBOX (determinista, sin depender del orden de PATH del shell): por cada nombre que
|
||||||
|
// el userland Rust provee en `/usr/bin`, retirar el symlink homónimo de busybox en los OTROS dirs
|
||||||
|
// (`/bin`, `/sbin`, `/usr/sbin`) — busybox suele instalar `ls` en `/bin`, fuera del `/usr/bin` que
|
||||||
|
// la hidratación ya ensombreció. Así la tool Rust es la ÚNICA en PATH. El binario busybox queda
|
||||||
|
// (sh/ash, tar, mount, … lo no reemplazado); sólo se quitan los applets ya cubiertos por Rust.
|
||||||
|
let mut applet_names: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
|
||||||
|
for (name, h) in userland {
|
||||||
|
let ub = store.path_of(h, name).join("usr/bin");
|
||||||
|
if let Ok(rd) = std::fs::read_dir(&ub) {
|
||||||
|
for e in rd.flatten() {
|
||||||
|
applet_names.insert(e.file_name().to_string_lossy().into_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for applet in &applet_names {
|
||||||
|
for d in ["bin", "sbin", "usr/sbin"] {
|
||||||
|
let p = staging.join(d).join(applet);
|
||||||
|
if std::fs::symlink_metadata(&p).is_ok() {
|
||||||
|
let _ = std::fs::remove_file(&p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3) seed de producto (sobrescribe la base) + configs + dirs de runtime de sshd.
|
// 3) seed de producto (sobrescribe la base) + configs + dirs de runtime de sshd.
|
||||||
// Los ficheros hidratados son HARDLINKS al store sellado (read-only); escribir encima daría
|
// Los ficheros hidratados son HARDLINKS al store sellado (read-only); escribir encima daría
|
||||||
// EACCES y `chmod` mutaría el inodo del store. Hay que ROMPER el hardlink: remove + write nuevo.
|
// EACCES y `chmod` mutaría el inodo del store. Hay que ROMPER el hardlink: remove + write nuevo.
|
||||||
@@ -532,31 +568,44 @@ fn assemble_product_rootfs(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// **Producto** — toma el rootfs base verificado (`base`, el `stage1-rootfs` del 4/4) y le inyecta la
|
/// Construye/cachea (idempotente vía el lab) una lista de componentes nombrados, devolviendo sus
|
||||||
/// capa de servicios por hidratación tardía, sellando un `product-rootfs`. El mecanismo base queda
|
/// `(nombre, hash)` sellados. Cada receta usa su propia config de build (p.ej. `zig_version`).
|
||||||
/// intacto: este árbol NO es el que reconstruye el selfhost-verify (su `of_tree` sigue blindado).
|
fn build_components(
|
||||||
/// Idempotente. `base_cfg` apunta el zig del lab (no la semilla): los servicios usan su `zig_version`
|
names: &[&str],
|
||||||
/// propia (openssh ⇒ 0.13.0), y si ya están sellados, `build` los devuelve cacheados sin recompilar.
|
recipes_dir: &Path,
|
||||||
|
cfg: &hammer_build::BuildConfig,
|
||||||
|
store: &Store,
|
||||||
|
) -> Result<Vec<(String, ArtifactHash)>> {
|
||||||
|
let mut out = Vec::with_capacity(names.len());
|
||||||
|
for name in names {
|
||||||
|
let recipe_path = recipes_dir.join(format!("{name}.toml"));
|
||||||
|
let recipe = Recipe::load_from_path(&recipe_path).map_err(|e| {
|
||||||
|
Error::Other(format!("receta '{name}' ({}): {e}", recipe_path.display()))
|
||||||
|
})?;
|
||||||
|
let h = hammer_build::build(&recipe, cfg, store)?;
|
||||||
|
out.push((name.to_string(), h));
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **Producto** — toma el rootfs base verificado (`base`, el `stage1-rootfs` del 4/4) y le inyecta por
|
||||||
|
/// hidratación tardía el userland Rust (adelgaza busybox) + la capa de servicios, sellando un
|
||||||
|
/// `product-rootfs`. El mecanismo base queda intacto: este árbol NO es el que reconstruye el
|
||||||
|
/// selfhost-verify (su `of_tree` sigue blindado). Idempotente. `base_cfg` apunta el zig del lab (no la
|
||||||
|
/// semilla): cada componente usa su `zig_version` propia, y si ya está sellado sale cacheado.
|
||||||
pub fn product(
|
pub fn product(
|
||||||
base: &RootfsHash,
|
base: &RootfsHash,
|
||||||
base_cfg: &hammer_build::BuildConfig,
|
base_cfg: &hammer_build::BuildConfig,
|
||||||
recipes_dir: &Path,
|
recipes_dir: &Path,
|
||||||
store: &Store,
|
store: &Store,
|
||||||
) -> Result<RootfsHash> {
|
) -> Result<RootfsHash> {
|
||||||
// 1) construir/cachear los componentes de servicio (idempotente vía el lab).
|
// 1) construir/cachear el userland Rust + los servicios.
|
||||||
let mut services: Vec<(String, ArtifactHash)> = Vec::with_capacity(SERVICE_COMPONENTS.len());
|
let userland = build_components(USERLAND_COMPONENTS, recipes_dir, base_cfg, store)?;
|
||||||
for name in SERVICE_COMPONENTS {
|
let services = build_components(SERVICE_COMPONENTS, recipes_dir, base_cfg, store)?;
|
||||||
let recipe_path = recipes_dir.join(format!("{name}.toml"));
|
|
||||||
let recipe = Recipe::load_from_path(&recipe_path).map_err(|e| {
|
|
||||||
Error::Other(format!("receta '{name}' ({}): {e}", recipe_path.display()))
|
|
||||||
})?;
|
|
||||||
let h = hammer_build::build(&recipe, base_cfg, store)?;
|
|
||||||
services.push((name.to_string(), h));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) seed de producto + hash de identidad.
|
// 2) seed de producto + hash de identidad.
|
||||||
let seed = product_seed_card()?;
|
let seed = product_seed_card()?;
|
||||||
let phash = product_rootfs_hash(base, &services, &seed);
|
let phash = product_rootfs_hash(base, &userland, &services, &seed);
|
||||||
let store_name = "product-rootfs";
|
let store_name = "product-rootfs";
|
||||||
|
|
||||||
// 3) ensamblar + sellar (si no estaba ya).
|
// 3) ensamblar + sellar (si no estaba ya).
|
||||||
@@ -568,13 +617,13 @@ pub fn product(
|
|||||||
let _ = std::fs::remove_dir_all(&staging);
|
let _ = std::fs::remove_dir_all(&staging);
|
||||||
std::fs::create_dir_all(&staging)?;
|
std::fs::create_dir_all(&staging)?;
|
||||||
let result = (|| -> Result<()> {
|
let result = (|| -> Result<()> {
|
||||||
assemble_product_rootfs(store, base, &services, &seed, &staging)?;
|
assemble_product_rootfs(store, base, &userland, &services, &seed, &staging)?;
|
||||||
store.seal(&staging, &phash, store_name)?;
|
store.seal(&staging, &phash, store_name)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})();
|
})();
|
||||||
let _ = std::fs::remove_dir_all(&staging);
|
let _ = std::fs::remove_dir_all(&staging);
|
||||||
result?;
|
result?;
|
||||||
tracing::info!(hash = %phash, "product: rootfs de servicios sellado");
|
tracing::info!(hash = %phash, "product: rootfs (userland Rust + servicios) sellado");
|
||||||
} else {
|
} else {
|
||||||
tracing::info!(hash = %phash, "product: rootfs ya sellado (idempotente)");
|
tracing::info!(hash = %phash, "product: rootfs ya sellado (idempotente)");
|
||||||
}
|
}
|
||||||
@@ -1526,14 +1575,17 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn product_rootfs_hash_is_deterministic_and_sensitive() {
|
fn product_rootfs_hash_is_deterministic_and_sensitive() {
|
||||||
let base = ArtifactHash::from_hex("ab");
|
let base = ArtifactHash::from_hex("ab");
|
||||||
|
let userland = [("uutils".to_string(), ArtifactHash::from_hex("11"))];
|
||||||
let svcs = [("openssh".to_string(), ArtifactHash::from_hex("cd"))];
|
let svcs = [("openssh".to_string(), ArtifactHash::from_hex("cd"))];
|
||||||
let seed = product_seed_card().unwrap();
|
let seed = product_seed_card().unwrap();
|
||||||
let a = product_rootfs_hash(&base, &svcs, &seed);
|
let a = product_rootfs_hash(&base, &userland, &svcs, &seed);
|
||||||
assert_eq!(a, product_rootfs_hash(&base, &svcs, &seed), "determinista");
|
assert_eq!(a, product_rootfs_hash(&base, &userland, &svcs, &seed), "determinista");
|
||||||
let other_base = ArtifactHash::from_hex("ef");
|
let other_base = ArtifactHash::from_hex("ef");
|
||||||
assert_ne!(a, product_rootfs_hash(&other_base, &svcs, &seed), "cambiar la base re-hashea");
|
assert_ne!(a, product_rootfs_hash(&other_base, &userland, &svcs, &seed), "cambiar la base re-hashea");
|
||||||
|
let other_userland = [("uutils".to_string(), ArtifactHash::from_hex("22"))];
|
||||||
|
assert_ne!(a, product_rootfs_hash(&base, &other_userland, &svcs, &seed), "cambiar el userland re-hashea");
|
||||||
let other_svc = [("openssh".to_string(), ArtifactHash::from_hex("99"))];
|
let other_svc = [("openssh".to_string(), ArtifactHash::from_hex("99"))];
|
||||||
assert_ne!(a, product_rootfs_hash(&base, &other_svc, &seed), "cambiar un servicio re-hashea");
|
assert_ne!(a, product_rootfs_hash(&base, &userland, &other_svc, &seed), "cambiar un servicio re-hashea");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1542,17 +1594,31 @@ mod tests {
|
|||||||
let store = Store::open(tmp.path().join("store")).unwrap();
|
let store = Store::open(tmp.path().join("store")).unwrap();
|
||||||
|
|
||||||
// Un rootfs base sintético sellado como `stage1-rootfs` (el 4/4 ya verificado): trae la seed
|
// Un rootfs base sintético sellado como `stage1-rootfs` (el 4/4 ya verificado): trae la seed
|
||||||
// base, /sbin/init, un mountpoint vacío y un binario del núcleo.
|
// base, /sbin/init, un mountpoint vacío, un binario del núcleo y un applet busybox `ls`.
|
||||||
let base = seal_component(&store, "stage1-rootfs", "ba5e", |w| {
|
let base = seal_component(&store, "stage1-rootfs", "ba5e", |w| {
|
||||||
std::fs::create_dir_all(w.join("usr/bin")).unwrap();
|
std::fs::create_dir_all(w.join("usr/bin")).unwrap();
|
||||||
|
std::fs::create_dir_all(w.join("bin")).unwrap();
|
||||||
std::fs::create_dir_all(w.join("ente")).unwrap();
|
std::fs::create_dir_all(w.join("ente")).unwrap();
|
||||||
std::fs::create_dir_all(w.join("proc")).unwrap();
|
std::fs::create_dir_all(w.join("proc")).unwrap();
|
||||||
std::fs::create_dir_all(w.join("var")).unwrap();
|
std::fs::create_dir_all(w.join("var")).unwrap();
|
||||||
std::fs::create_dir_all(w.join("root")).unwrap();
|
std::fs::create_dir_all(w.join("root")).unwrap();
|
||||||
std::fs::write(w.join("usr/bin/hammerd"), b"\x7fELFfake").unwrap();
|
std::fs::write(w.join("usr/bin/hammerd"), b"\x7fELFfake").unwrap();
|
||||||
|
std::fs::write(w.join("bin/busybox"), b"\x7fELFbb").unwrap();
|
||||||
|
// busybox instala el applet `ls` como symlink en /usr/bin (lo ensombrece la hidratación) y
|
||||||
|
// `cat` en /bin (lo debe RETIRAR el adelgazamiento). `sh` en /bin NO se reemplaza (queda).
|
||||||
|
std::os::unix::fs::symlink("../../bin/busybox", w.join("usr/bin/ls")).unwrap();
|
||||||
|
std::os::unix::fs::symlink("busybox", w.join("bin/cat")).unwrap();
|
||||||
|
std::os::unix::fs::symlink("busybox", w.join("bin/sh")).unwrap();
|
||||||
std::fs::write(w.join("ente/seed.card.json"), STAGE1_SEED_CARD).unwrap();
|
std::fs::write(w.join("ente/seed.card.json"), STAGE1_SEED_CARD).unwrap();
|
||||||
std::os::unix::fs::symlink("/usr/bin/arje-zero", w.join("sbin_init_tmp")).unwrap();
|
std::os::unix::fs::symlink("/usr/bin/arje-zero", w.join("sbin_init_tmp")).unwrap();
|
||||||
});
|
});
|
||||||
|
// Userland Rust sintético (uutils): coreutils multicall + symlink `ls`→coreutils.
|
||||||
|
let huu = seal_component(&store, "uutils", "c0re", |w| {
|
||||||
|
std::fs::create_dir_all(w.join("usr/bin")).unwrap();
|
||||||
|
std::fs::write(w.join("usr/bin/coreutils"), b"\x7fELFuu").unwrap();
|
||||||
|
std::os::unix::fs::symlink("coreutils", w.join("usr/bin/ls")).unwrap();
|
||||||
|
std::os::unix::fs::symlink("coreutils", w.join("usr/bin/cat")).unwrap();
|
||||||
|
});
|
||||||
// Componentes de servicio sintéticos.
|
// Componentes de servicio sintéticos.
|
||||||
let hossh = seal_component(&store, "openssh", "0551", |w| {
|
let hossh = seal_component(&store, "openssh", "0551", |w| {
|
||||||
std::fs::create_dir_all(w.join("usr/sbin")).unwrap();
|
std::fs::create_dir_all(w.join("usr/sbin")).unwrap();
|
||||||
@@ -1564,16 +1630,29 @@ mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let seed = product_seed_card().unwrap();
|
let seed = product_seed_card().unwrap();
|
||||||
|
let userland = [("uutils".to_string(), huu)];
|
||||||
let services = [("openssh".to_string(), hossh), ("netup".to_string(), hnet)];
|
let services = [("openssh".to_string(), hossh), ("netup".to_string(), hnet)];
|
||||||
let staging = store.root().join("prod-staging");
|
let staging = store.root().join("prod-staging");
|
||||||
std::fs::create_dir_all(&staging).unwrap();
|
std::fs::create_dir_all(&staging).unwrap();
|
||||||
assemble_product_rootfs(&store, &base, &services, &seed, &staging).unwrap();
|
assemble_product_rootfs(&store, &base, &userland, &services, &seed, &staging).unwrap();
|
||||||
|
|
||||||
// base hidratada + servicios inyectados.
|
// base hidratada + userland + servicios inyectados.
|
||||||
assert!(staging.join("usr/bin/hammerd").is_file(), "núcleo presente");
|
assert!(staging.join("usr/bin/hammerd").is_file(), "núcleo presente");
|
||||||
assert!(staging.join("proc").is_dir(), "mountpoint vacío recreado");
|
assert!(staging.join("proc").is_dir(), "mountpoint vacío recreado");
|
||||||
|
assert!(staging.join("usr/bin/coreutils").is_file(), "uutils inyectado");
|
||||||
assert!(staging.join("usr/sbin/sshd").is_file(), "openssh inyectado");
|
assert!(staging.join("usr/sbin/sshd").is_file(), "openssh inyectado");
|
||||||
assert!(staging.join("usr/bin/netup").is_file(), "netup inyectado");
|
assert!(staging.join("usr/bin/netup").is_file(), "netup inyectado");
|
||||||
|
// ADELGAZAR BUSYBOX: /usr/bin/ls apunta ahora a coreutils (Rust), no a busybox.
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_link(staging.join("usr/bin/ls")).unwrap(),
|
||||||
|
std::path::PathBuf::from("coreutils"),
|
||||||
|
"el userland Rust ensombrece el applet busybox en /usr/bin"
|
||||||
|
);
|
||||||
|
// El applet busybox `cat` en /bin fue RETIRADO (uutils provee cat) ⇒ no compite en PATH.
|
||||||
|
assert!(!staging.join("bin/cat").exists(), "el applet busybox /bin/cat reemplazado fue retirado");
|
||||||
|
// Lo NO reemplazado por Rust (sh) y el binario busybox quedan intactos.
|
||||||
|
assert!(staging.join("bin/sh").exists(), "/bin/sh (ash) interino se preserva");
|
||||||
|
assert!(staging.join("bin/busybox").is_file(), "el binario busybox queda (fallback)");
|
||||||
// la seed fue sobrescrita con la de producto (3 cards).
|
// la seed fue sobrescrita con la de producto (3 cards).
|
||||||
let got = std::fs::read_to_string(staging.join("ente/seed.card.json")).unwrap();
|
let got = std::fs::read_to_string(staging.join("ente/seed.card.json")).unwrap();
|
||||||
let v: serde_json::Value = serde_json::from_str(&got).unwrap();
|
let v: serde_json::Value = serde_json::from_str(&got).unwrap();
|
||||||
@@ -1590,12 +1669,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shipped_service_recipes_parse_and_hash() {
|
fn shipped_product_recipes_parse_and_hash() {
|
||||||
// Las recetas de servicio reales (netup, openssh) cargan y hashean — no contaminan el núcleo.
|
// Las recetas de producto reales (userland Rust + servicios) cargan y hashean — viven fuera
|
||||||
|
// del núcleo (no entran a STAGE1_COMPONENTS ni al selfhost-verify).
|
||||||
let recipes_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../recipes");
|
let recipes_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../recipes");
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let store = Store::open(tmp.path().join("store")).unwrap();
|
let store = Store::open(tmp.path().join("store")).unwrap();
|
||||||
for name in SERVICE_COMPONENTS {
|
for name in USERLAND_COMPONENTS.iter().chain(SERVICE_COMPONENTS.iter()) {
|
||||||
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);
|
||||||
|
|||||||
@@ -42,10 +42,13 @@ chmod 0600 "$RFS/root/.ssh/authorized_keys"
|
|||||||
|
|
||||||
# sanity: el árbol de producto trae lo que esperamos (sin ensamblarlo acá)
|
# sanity: el árbol de producto trae lo que esperamos (sin ensamblarlo acá)
|
||||||
echo "==> chequeo de contenido del artefacto:"
|
echo "==> chequeo de contenido del artefacto:"
|
||||||
for f in usr/sbin/sshd usr/bin/netup etc/ssh/sshd_config etc/passwd ente/seed.card.json; do
|
for f in usr/sbin/sshd usr/bin/netup usr/bin/coreutils usr/bin/find usr/bin/rg etc/ssh/sshd_config etc/passwd ente/seed.card.json; do
|
||||||
[ -e "$RFS/$f" ] && echo " ok $f" || { echo " FALTA $f"; exit 1; }
|
[ -e "$RFS/$f" ] && echo " ok $f" || { echo " FALTA $f"; exit 1; }
|
||||||
done
|
done
|
||||||
grep -q '"label": "sshd"' "$RFS/ente/seed.card.json" && echo " ok card sshd en la seed" || { echo " FALTA card sshd"; exit 1; }
|
grep -q '"label": "sshd"' "$RFS/ente/seed.card.json" && echo " ok card sshd en la seed" || { echo " FALTA card sshd"; exit 1; }
|
||||||
|
# adelgazar busybox: /usr/bin/ls debe apuntar a coreutils (Rust), no a busybox
|
||||||
|
lstgt=$(readlink "$RFS/usr/bin/ls" 2>/dev/null || true)
|
||||||
|
[ "$lstgt" = coreutils ] && echo " ok /usr/bin/ls → coreutils (ensombrece busybox)" || echo " !! /usr/bin/ls → ${lstgt:-?}"
|
||||||
|
|
||||||
# empaquetar (root-owned) y bootear
|
# empaquetar (root-owned) y bootear
|
||||||
( cd "$RFS" && find . -print0 | cpio --null -o -H newc -R 0:0 2>/dev/null | gzip -1 ) > "$WORK/initramfs.cpio.gz"
|
( cd "$RFS" && find . -print0 | cpio --null -o -H newc -R 0:0 2>/dev/null | gzip -1 ) > "$WORK/initramfs.cpio.gz"
|
||||||
@@ -63,7 +66,10 @@ start=$(date +%s); ok=0
|
|||||||
while [ $(( $(date +%s) - start )) -lt "$DEADLINE" ]; do
|
while [ $(( $(date +%s) - start )) -lt "$DEADLINE" ]; do
|
||||||
kill -0 $QPID 2>/dev/null || { echo "!! QEMU murió"; break; }
|
kill -0 $QPID 2>/dev/null || { echo "!! QEMU murió"; break; }
|
||||||
if out=$(ssh "${SSHOPTS[@]}" root@localhost \
|
if out=$(ssh "${SSHOPTS[@]}" root@localhost \
|
||||||
'echo PRODUCT_SSH_OK uid=$(id -u); cat /ente/seed.card.json | grep -o "\"label\": \"[a-z-]*\"" | tr "\n" " "; echo; uname -sr' 2>/dev/null); then
|
'echo PRODUCT_SSH_OK uid=$(id -u); uname -sr;
|
||||||
|
printf "ls: "; command -v ls; ls --version 2>&1 | head -1;
|
||||||
|
printf "find: "; find --version 2>&1 | head -1;
|
||||||
|
printf "rg: "; rg --version 2>&1 | head -1' 2>/dev/null); then
|
||||||
echo "============== RESPUESTA DEL GUEST =============="; echo "$out"; echo "================================================"
|
echo "============== RESPUESTA DEL GUEST =============="; echo "$out"; echo "================================================"
|
||||||
echo "$out" | grep -q PRODUCT_SSH_OK && ok=1; break
|
echo "$out" | grep -q PRODUCT_SSH_OK && ok=1; break
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user