selfhost-verify: pieza 4 (bwrap swap) + materialización de build-deps en el lab
Cuarta pieza del toolchain hammer-from-source (variante b, SDD 11 §7.2b): bubblewrap, el
sandbox del propio lab. Binario estático ⇒ swap de archivo sobre /toolchain/usr/bin/bwrap.
Como herramienta del toolchain (no input del 4/4) no necesita casar byte-a-byte con Alpine,
sólo aislar igual.
Tres piezas:
- recipes/libcap.toml (2.78): dep obligatoria de bwrap; el toolchain Alpine no trae el -dev
(libcap.a / sys/capability.h). Build estático musl con zig cc, sin patches (es tool, no input).
- materialización de build-deps (hammer-build): deps.build ahora se CONSTRUYE recursivamente
(build() llama build() por cada dep) y cada artefacto sellado se apila como capa --overlay-src
bajo el rootfs del sandbox, dejando usr/{include,lib,lib/pkgconfig} en /usr. pkgconf y zig cc
las hallan sin plumbing de flags. Recetas sin deps: sandbox byte-igual (baseline intacto).
Tests nuevos: no_deps_emits_single_overlay_src, deps_stack_as_overlay_layers_under_rootfs.
- recipes/bwrap.toml (0.11.0): el toolchain no trae meson/ninja/python, así que bypaseamos meson
compilando los 4 .c de bubblewrap directo con zig cc (+config.h trivial). deps.build=["libcap"].
Validación host fuerte: hammer-bwrap es estático, corre --version y sandboxea, y musl rebuildeó
BYTE-IDÉNTICO usándolo de sandbox (bisección). Expuesto con SWAP_BWRAP=1. Tests verdes.
Pendiente: corrida in-VM acumulando swaps para el sello ✓ REPRODUCIBLE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,41 @@ fn resolve_build_dep_hashes(
|
||||
Ok(hashes)
|
||||
}
|
||||
|
||||
/// Construye (si falta) cada `deps.build` y devuelve sus directorios sellados en el store, para
|
||||
/// apilarlos como capas overlay del sandbox (variante b: bwrap necesita libcap.a + sys/capability.h,
|
||||
/// ausentes del toolchain Alpine). Recursivo vía `build`: una dep con sus propias deps se materializa
|
||||
/// transitivamente. Los ciclos ya los corta `artifact_hash` (con su `stack`) antes de llegar acá.
|
||||
fn materialize_build_deps(
|
||||
recipe: &Recipe,
|
||||
cfg: &BuildConfig,
|
||||
store: &Store,
|
||||
) -> hammer_core::Result<Vec<PathBuf>> {
|
||||
if recipe.deps.build.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if recipe.base_dir.as_os_str().is_empty() {
|
||||
return Err(hammer_core::Error::Recipe(format!(
|
||||
"la receta '{}' declara deps de build {:?} pero no tiene base_dir; \
|
||||
cárgala con Recipe::load_from_path para resolver el catálogo",
|
||||
recipe.name, recipe.deps.build
|
||||
)));
|
||||
}
|
||||
let mut dirs = Vec::with_capacity(recipe.deps.build.len());
|
||||
for dep in &recipe.deps.build {
|
||||
let dep_path = recipe.base_dir.join(format!("{dep}.toml"));
|
||||
let dep_recipe = Recipe::load_from_path(&dep_path).map_err(|e| {
|
||||
hammer_core::Error::Recipe(format!(
|
||||
"no pude cargar la dep de build '{dep}' de '{}' ({}): {e}",
|
||||
recipe.name,
|
||||
dep_path.display()
|
||||
))
|
||||
})?;
|
||||
let dep_hash = build(&dep_recipe, cfg, store)?;
|
||||
dirs.push(store.path_of(&dep_hash, &dep_recipe.name));
|
||||
}
|
||||
Ok(dirs)
|
||||
}
|
||||
|
||||
/// Compila una receta y devuelve el hash del artefacto sellado en el store.
|
||||
/// Si el hash ya existe en el store, devuelve sin recompilar (caché).
|
||||
pub fn build(
|
||||
@@ -96,6 +131,13 @@ pub fn build(
|
||||
}
|
||||
sandbox::ensure_layout(&cfg.rootfs, &cfg.zig_dir)?;
|
||||
|
||||
// Materializa las deps de build (las construye si faltan) ANTES del fetch: sus árboles sellados
|
||||
// se apilan como capas overlay del sandbox para que headers/libs/pkgconfig estén en /usr.
|
||||
let dep_dirs = materialize_build_deps(recipe, cfg, store)?;
|
||||
if !dep_dirs.is_empty() {
|
||||
tracing::info!(n = dep_dirs.len(), "deps de build materializadas como capas overlay");
|
||||
}
|
||||
|
||||
tracing::info!(hash = %h, name = %recipe.name, "fetch");
|
||||
let src_tree = fetch::fetch(recipe, &cfg.work_root)?;
|
||||
|
||||
@@ -127,6 +169,7 @@ pub fn build(
|
||||
src_dir: src_tree.clone(),
|
||||
out_dir: out_dir.clone(),
|
||||
cache_dir: cfg.cache_root.clone(),
|
||||
deps: dep_dirs,
|
||||
env,
|
||||
};
|
||||
|
||||
|
||||
@@ -66,6 +66,11 @@ pub struct Sandbox {
|
||||
/// Si Some, se bindea RW como `/cache` y se setea `ZIG_GLOBAL_CACHE_DIR=/cache/zig`.
|
||||
/// Persistir esta caché entre builds evita que `zig cc` recompile musl cada vez (~30s).
|
||||
pub cache_dir: Option<PathBuf>,
|
||||
/// Directorios sellados de las `deps.build` (artefactos ya construidos en el store). Cada uno se
|
||||
/// apila como capa overlay read-only BAJO el rootfs Alpine, así sus `usr/{include,lib,lib/pkgconfig}`
|
||||
/// aparecen en las rutas estándar de `/usr` — el compilador y pkgconf los ven sin plumbing de flags.
|
||||
/// Vacío ⇒ ninguna capa extra ⇒ el sandbox es idéntico al de una receta sin deps (baseline intacto).
|
||||
pub deps: Vec<PathBuf>,
|
||||
/// Vars de entorno extra inyectadas además del set por defecto.
|
||||
pub env: Vec<(String, String)>,
|
||||
}
|
||||
@@ -125,6 +130,15 @@ impl Sandbox {
|
||||
let mut args: Vec<String> = vec![
|
||||
"--overlay-src".into(),
|
||||
self.rootfs.display().to_string(),
|
||||
];
|
||||
// Capas overlay de las deps de build, apiladas bajo el rootfs (mismo `--tmp-overlay /`). bwrap
|
||||
// admite múltiples `--overlay-src` para un overlay; los `usr/{include,lib}` de cada dep se
|
||||
// funden con los del rootfs en `/usr`. Sólo se emiten si hay deps (compat hacia atrás).
|
||||
for dep in &self.deps {
|
||||
args.push("--overlay-src".into());
|
||||
args.push(dep.display().to_string());
|
||||
}
|
||||
args.extend([
|
||||
"--tmp-overlay".into(),
|
||||
"/".into(),
|
||||
"--proc".into(),
|
||||
@@ -146,7 +160,7 @@ impl Sandbox {
|
||||
"/src".into(),
|
||||
"--unshare-all".into(),
|
||||
"--die-with-parent".into(),
|
||||
];
|
||||
]);
|
||||
|
||||
if let Some(cache) = &self.cache_dir {
|
||||
args.push("--bind".into());
|
||||
@@ -304,4 +318,45 @@ mod tests {
|
||||
let err = hammer_core::Error::Recipe("nope".into());
|
||||
assert!(BuildFailure::from_error(&err).is_none());
|
||||
}
|
||||
|
||||
fn fixture(deps: Vec<PathBuf>) -> Sandbox {
|
||||
Sandbox {
|
||||
rootfs: PathBuf::from("/rootfs"),
|
||||
zig_dir: PathBuf::from("/zig"),
|
||||
src_dir: PathBuf::from("/src-host"),
|
||||
out_dir: PathBuf::from("/out-host"),
|
||||
cache_dir: None,
|
||||
deps,
|
||||
env: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_deps_emits_single_overlay_src() {
|
||||
// Compat hacia atrás: sin deps, una sola capa overlay (el rootfs) — sandbox idéntico al previo.
|
||||
let args = fixture(vec![]).bwrap_args("true");
|
||||
let n = args.iter().filter(|a| *a == "--overlay-src").count();
|
||||
assert_eq!(n, 1, "sin deps ⇒ un único --overlay-src");
|
||||
// El --tmp-overlay viene inmediatamente tras el rootfs.
|
||||
let i = args.iter().position(|a| a == "--overlay-src").unwrap();
|
||||
assert_eq!(args[i + 1], "/rootfs");
|
||||
assert_eq!(args[i + 2], "--tmp-overlay");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deps_stack_as_overlay_layers_under_rootfs() {
|
||||
let args = fixture(vec![PathBuf::from("/dep/libcap"), PathBuf::from("/dep/zlib")])
|
||||
.bwrap_args("true");
|
||||
// rootfs + 2 deps = 3 capas overlay, todas antes del --tmp-overlay.
|
||||
let srcs: Vec<&String> = args
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, a)| *a == "--overlay-src")
|
||||
.map(|(i, _)| &args[i + 1])
|
||||
.collect();
|
||||
assert_eq!(srcs, vec!["/rootfs", "/dep/libcap", "/dep/zlib"]);
|
||||
let to = args.iter().position(|a| a == "--tmp-overlay").unwrap();
|
||||
let last_src = args.iter().rposition(|a| a == "--overlay-src").unwrap();
|
||||
assert!(last_src < to, "todas las capas overlay van antes del --tmp-overlay");
|
||||
}
|
||||
}
|
||||
|
||||
+16
-3
@@ -269,9 +269,22 @@ Diseño completo en [SDD 11 — Bootstrap from-scratch](11-bootstrap.md). Resume
|
||||
se pisan sobre la salida. Criterio de éxito fuerte alcanzado: **`diff -r` del header-tree hammer contra
|
||||
el de Alpine = vacío** ⇒ el toolchain swapeado es byte-idéntico al baseline ⇒ todo lo que compile abajo
|
||||
reproduce de por sí (garantía por construcción, más fuerte que un rebuild). Expuesto con
|
||||
`SWAP_LINUX_HEADERS=1`; ensamblado end-to-end en host OK. **Pendiente:** la corrida in-VM
|
||||
(`KVM=1 MEM=24576 SWAP_LINUX_HEADERS=1 ./scripts/selfhost-verify.sh`) para el sello `✓ REPRODUCIBLE`,
|
||||
y seguir con bwrap → rust/llvm (cada una swapeada y re-verificada igual).
|
||||
`SWAP_LINUX_HEADERS=1`; ensamblado end-to-end en host OK. Falta sólo la corrida in-VM para el sello.
|
||||
**Pieza 4 — bwrap (`recipes/bwrap.toml`, bubblewrap 0.11.0, validada en host):** EL sandbox del propio
|
||||
lab (`hammer-build/src/sandbox.rs`). Binario estático ⇒ swap de archivo sobre `/toolchain/usr/bin/bwrap`.
|
||||
Como tool del toolchain (no input del 4/4) NO necesita casar byte-a-byte con Alpine, sólo aislar igual.
|
||||
Tres sub-hallazgos que la hicieron más jugosa que make/busybox: (1) el toolchain Alpine no trae meson/
|
||||
ninja/python — bypaseamos meson compilando los 4 `.c` de bubblewrap directo con `zig cc` (+`config.h`
|
||||
trivial generado a mano); (2) depende de **libcap**, cuyo -dev (libcap.a + `sys/capability.h`) tampoco
|
||||
está en el toolchain ⇒ nueva receta `recipes/libcap.toml`; (3) para que el build de bwrap viera libcap se
|
||||
agregó **materialización de build-deps** al lab: `deps.build` ahora se construye recursivamente y cada
|
||||
artefacto sellado se apila como capa `--overlay-src` BAJO el rootfs del sandbox, dejando sus
|
||||
`usr/{include,lib,lib/pkgconfig}` en `/usr` — pkgconf y zig cc los hallan sin plumbing de flags (recetas
|
||||
sin deps quedan byte-iguales, baseline intacto). Validación host fuerte: hammer-bwrap es estático, corre
|
||||
`--version`/sandboxea, y **musl rebuildeó byte-idéntico** usándolo de sandbox (bisección). Expuesto con
|
||||
`SWAP_BWRAP=1`. **Pendiente:** correr in-VM acumulando los swaps
|
||||
(`KVM=1 MEM=24576 SWAP_MAKE=1 SWAP_BUSYBOX=1 SWAP_LINUX_HEADERS=1 SWAP_BWRAP=1 ./scripts/selfhost-verify.sh`)
|
||||
para el `✓ REPRODUCIBLE`, y seguir con la última pieza: rust/llvm (la grande — ya hay infra de deps).
|
||||
- ⏭️ **También pendiente (Stage 1):** bus único (B.2: exponer el `CRASHED` a la capa de IA) y
|
||||
atestación arje (A1/A2).
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# bubblewrap (bwrap) 0.11.0 — pieza 4 del toolchain hammer-from-source (SDD 11 §7.2b).
|
||||
#
|
||||
# bwrap es EL sandbox del lab: cada build hermético corre dentro de `bwrap` (ver
|
||||
# hammer-build/src/sandbox.rs). Reemplazar el bwrap de Alpine por uno construido por hammer cierra
|
||||
# otra dependencia del toolchain. Como make/busybox/linux-headers, se monta sobre /toolchain con el
|
||||
# swap del builder (variante b) — aquí un swap de BINARIO (`--swap bwrap=<hash>:usr/bin/bwrap`).
|
||||
#
|
||||
# bwrap NO necesita casar byte-a-byte con el de Alpine: es una herramienta del toolchain, no un input
|
||||
# del 4/4. Cualquier bwrap funcional aísla igual (mismos namespaces/mounts ⇒ mismos outputs), así que
|
||||
# la reproducibilidad de of_tree(stage1') se mantiene mientras bwrap sandboxee correctamente.
|
||||
#
|
||||
# Por qué NO meson: bubblewrap 0.11 usa meson+ninja+python, AUSENTES del toolchain Alpine. Pero el
|
||||
# proyecto es 4 archivos .c + un `config.h` trivial; los compilamos directo con `zig cc` estático,
|
||||
# evitando el meson por completo. Es el mismo binario, sólo otro driver de build.
|
||||
#
|
||||
# libcap es dependencia obligatoria (bubblewrap.c hace `#include <sys/capability.h>` y llama `cap_*`).
|
||||
# El toolchain Alpine trae sólo el .so runtime, no el -dev; por eso `deps.build = ["libcap"]`: el lab
|
||||
# construye libcap y apila su árbol como capa overlay del sandbox, dejando libcap.a + sys/capability.h
|
||||
# + libcap.pc en /usr — pkgconf y zig cc los ven en las rutas estándar (ver materialize_build_deps).
|
||||
|
||||
name = "bwrap"
|
||||
version = "0.11.0"
|
||||
|
||||
[source]
|
||||
tarball = "https://github.com/containers/bubblewrap/releases/download/v0.11.0/bubblewrap-0.11.0.tar.xz"
|
||||
sha256 = "988fd6b232dafa04b8b8198723efeaccdb3c6aa9c1c7936219d5791a8b7a8646"
|
||||
|
||||
[build]
|
||||
compiler = "zig-cc"
|
||||
target = "x86_64-linux-musl"
|
||||
link = "static"
|
||||
|
||||
[deps]
|
||||
build = ["libcap"]
|
||||
|
||||
[build.phases]
|
||||
# El árbol detecta como BuildSys::Meson ⇒ el default `meson setup` fallaría (no hay meson). Override
|
||||
# de las 3 fases para compilar a mano con zig cc.
|
||||
# configure: generar el `config.h` que meson produciría. Sólo PACKAGE_STRING es necesario; HAVE_SELINUX
|
||||
# y ENABLE_REQUIRE_USERNS son #ifdef opcionales (los dejamos sin definir: sin SELinux, sin require_userns).
|
||||
configure = "printf '#define PACKAGE_STRING \"bubblewrap 0.11.0\"\\n' > config.h"
|
||||
# compile: los 4 .c estáticos contra musl + libcap. -D_GNU_SOURCE lo añade meson (bwrap usa clearenv,
|
||||
# MS_*, etc.). -lcap -lpsx desde /usr/lib (la capa overlay de la dep libcap). -mcpu=baseline por
|
||||
# reproducibilidad CPU-independiente (igual que el resto del lab).
|
||||
compile = "zig cc -mcpu=baseline -static -O2 -D_GNU_SOURCE -I. bubblewrap.c bind-mount.c network.c utils.c -L/usr/lib -lcap -lpsx -o bwrap"
|
||||
# install: el binario a /usr/bin/bwrap, donde lo espera el swap del builder.
|
||||
install = "mkdir -p /out/usr/bin && cp bwrap /out/usr/bin/bwrap"
|
||||
@@ -0,0 +1,35 @@
|
||||
# libcap 2.78 — dependencia de build de bwrap (pieza 4 del toolchain hammer-from-source, SDD 11 §7.2b).
|
||||
#
|
||||
# bubblewrap usa libcap para soltar/levantar capabilities al armar el sandbox; su `bubblewrap.c` hace
|
||||
# `#include <sys/capability.h>` y llama `cap_*`. El toolchain Alpine trae el .so runtime pero NO el
|
||||
# -dev (ni `sys/capability.h` ni `libcap.a`), así que para compilar bwrap estático necesitamos libcap
|
||||
# construido por hammer y materializado en el sandbox (ver deps.build de bwrap.toml + sandbox.rs).
|
||||
#
|
||||
# A diferencia de linux-headers, libcap NO necesita casar byte-a-byte con la de Alpine: es una pieza
|
||||
# del TOOLCHAIN (se enlaza dentro de bwrap, una herramienta), no un input del 4/4. Cualquier libcap
|
||||
# funcional sirve mientras bwrap aísle igual. Por eso sin patches de Alpine: build vainilla y listo.
|
||||
#
|
||||
# Build estático musl con zig cc. libcap usa un Makefile plano (no autotools): hay que pasar los
|
||||
# overrides de toolchain por línea de comando y apagar lo que pide deps ausentes:
|
||||
# GOLANG=no — sin bindings Go (no hay go en el sandbox).
|
||||
# PAM_CAP=no — sin el módulo PAM (no hay libpam).
|
||||
# SHARED=no — sólo estático; no generamos .so (bwrap linkea libcap.a).
|
||||
# BUILD_CC — el compilador HOST para los toolitos generadores (_makenames/mkconst); zig cc igual.
|
||||
|
||||
name = "libcap"
|
||||
version = "2.78"
|
||||
|
||||
[source]
|
||||
tarball = "https://mirrors.edge.kernel.org/pub/linux/libs/security/linux-privs/libcap2/libcap-2.78.tar.gz"
|
||||
sha256 = "2a2c705e382c413643a458b837575c0eb0989477ab6fb99c87adbe9a259612ad"
|
||||
|
||||
[build]
|
||||
compiler = "zig-cc"
|
||||
target = "x86_64-linux-musl"
|
||||
link = "static"
|
||||
|
||||
[build.phases]
|
||||
# El árbol de libcap detecta como BuildSys::Make ⇒ el compile/install por defecto no pasa los
|
||||
# overrides de toolchain ni las flags GOLANG/PAM. Override explícito de ambas fases.
|
||||
compile = "make CC='zig cc -mcpu=baseline' BUILD_CC='zig cc -mcpu=baseline' AR='zig ar' RANLIB='zig ranlib' OBJCOPY='zig objcopy' GOLANG=no PAM_CAP=no SHARED=no lib=lib"
|
||||
install = "make CC='zig cc -mcpu=baseline' BUILD_CC='zig cc -mcpu=baseline' AR='zig ar' RANLIB='zig ranlib' OBJCOPY='zig objcopy' GOLANG=no PAM_CAP=no SHARED=no lib=lib prefix=/usr DESTDIR=/out install"
|
||||
@@ -117,9 +117,14 @@ done
|
||||
# SWAP_BUSYBOX=1 → monta el busybox de hammer sobre /toolchain/bin/busybox (los symlinks de
|
||||
# applets de Alpine — sh/sed/grep/awk/tar/find — pasan a usarlo; cp/mkdir/
|
||||
# install siguen siendo GNU coreutils, intactos).
|
||||
# SWAP_LINUX_HEADERS=1 → construye recipes/linux-headers.toml y swap-directorio de los 13 subdirs
|
||||
# kernel-owned de /toolchain/usr/include (linux/asm/…); musl bits/sys intactos.
|
||||
# SWAP_BWRAP=1 → construye recipes/bwrap.toml (+libcap, su dep, vía deps.build) y lo monta
|
||||
# sobre /toolchain/usr/bin/bwrap (el sandbox del propio lab).
|
||||
# SWAPS="name=hash[:rel] …" → swaps explícitos extra (formato del flag --swap).
|
||||
# Por defecto, off ⇒ corrida pura-Alpine (variante a), idéntica a la baseline conocida-buena.
|
||||
# Ambas piezas validadas en host: con make+busybox de hammer los 4/4 reproducen of_tree=9adefb82.
|
||||
# Piezas validadas en host: make+busybox reproducen of_tree=9adefb82 in-VM; linux-headers da un
|
||||
# header-tree byte-idéntico a Alpine (diff -r vacío); bwrap rebuildea musl byte-idéntico de sandbox.
|
||||
SWAP_ARGS=()
|
||||
if [[ "${SWAP_MAKE:-0}" == 1 ]]; then
|
||||
say "variante b — construir make desde fuente (recipes/make.toml) y swapearlo en /toolchain"
|
||||
@@ -152,6 +157,17 @@ if [[ "${SWAP_LINUX_HEADERS:-0}" == 1 ]]; then
|
||||
SWAP_ARGS+=(--swap "linux-headers=${LH_HASH}:usr/include/$sub")
|
||||
done
|
||||
fi
|
||||
if [[ "${SWAP_BWRAP:-0}" == 1 ]]; then
|
||||
# Pieza 4: bwrap (bubblewrap), EL sandbox del lab. Binario estático ⇒ swap de archivo sobre
|
||||
# /toolchain/usr/bin/bwrap. Su dep libcap la construye y materializa el lab solo (deps.build).
|
||||
# bwrap es herramienta, no input del 4/4: no necesita casar byte-a-byte con Alpine, sólo aislar
|
||||
# igual (validado en host — musl rebuildeó byte-idéntico bajo hammer-bwrap).
|
||||
say "variante b — construir bwrap (+libcap) desde fuente y swapearlo en /toolchain/usr/bin/bwrap"
|
||||
BW_HASH="$("$HAMMER" --store "$STORE" build recipes/bwrap.toml | grep -oE 'b3:[0-9a-f]{64}' | tail -1)"
|
||||
[[ "$BW_HASH" == b3:* ]] || die "no obtuve el hash sellado de bwrap"
|
||||
say "bwrap hammer: $BW_HASH"
|
||||
SWAP_ARGS+=(--swap "bwrap=${BW_HASH}:usr/bin/bwrap")
|
||||
fi
|
||||
for s in ${SWAPS:-}; do SWAP_ARGS+=(--swap "$s"); done
|
||||
|
||||
# 4) Ensamblar el builder con la referencia embebida.
|
||||
|
||||
Reference in New Issue
Block a user