> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
harness: wrapper linker-driver que quita -static en links -shared
Las extensiones Python (python.extension_module → shared_module) son .so que el
intérprete del sandbox debe dlopen; intrínsecamente dinámicas. El harness inyecta
LDFLAGS=-static para link=static, y meson/autotools lo aplican a TODOS los targets
por igual ⇒ el .so sale ET_EXEC y dlopen da "Exec format error" (el muro de
gobject-introspection: _giscanner.cpython-312.so no cargaba).
Fix estructural (elegido sobre flip-a-dynamic): ensure_layout materializa en el
rootfs dos wrappers (hammer-zig-cc/-cxx sobre zig cc/c++) que QUITAN -static SÓLO
cuando el link lleva -shared. -static+-shared es contradictorio ⇒ correcto por
construcción: transparente (byte-idéntico) para todo link normal, arregla TODA
extensión Python futura, no sólo g-i.
Hash-safe: el ArtifactHash se deriva de inputs (receta+deps), no de los bytes ⇒
no re-hashea ninguno de los 700+ sellados (cache-hit intacto); sólo cambia builds
nuevos con .so dinámicos. Verificado: rota positional params, preserva orden y
espacios; transparente sin -shared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
diff --git a/crates/hammer-build/src/sandbox.rs b/crates/hammer-build/src/sandbox.rs
index efabadfa..3bdc9fed 100644
--- a/crates/hammer-build/src/sandbox.rs
+++ b/crates/hammer-build/src/sandbox.rs
@@ -417,8 +417,11 @@ impl Sandbox {
// rompe la reproducibilidad — un no-determinismo que Stage 2 cazó (runbook §8c, SDD 09
// §2). De paso cierra el SIGILL de AVX en qemu64 (runbook §7). Las rutas SIMD que sí
// necesita el runtime (blake3/sha2) son asm con dispatch en runtime: siguen presentes.
- ("CC", "zig cc -mcpu=baseline"),
- ("CXX", "zig c++ -mcpu=baseline"),
+ // Driver vía wrapper (ensure_layout lo materializa en /usr/local/bin): idéntico a
+ // `zig cc -mcpu=baseline` salvo que quita `-static` en links `-shared` (extensiones
+ // Python y demás `.so` dlopen-ables). Ver write_cc_wrappers.
+ ("CC", CC_WRAPPER),
+ ("CXX", CXX_WRAPPER),
("AR", "zig ar"),
("LC_ALL", "C"),
("LANG", "C"),
@@ -517,6 +520,61 @@ pub fn ensure_layout(rootfs: &Path, zig_dir: &Path) -> hammer_core::Result<()> {
zig_dir.display()
)));
}
+ write_cc_wrappers(rootfs)?;
+ Ok(())
+}
+
+/// Nombres (en `/usr/local/bin`, que está en el PATH del sandbox) del driver de compilador que
+/// usan CC/CXX por defecto. Ver [`write_cc_wrappers`].
+pub const CC_WRAPPER: &str = "hammer-zig-cc";
+pub const CXX_WRAPPER: &str = "hammer-zig-cxx";
+
+/// Materializa en el rootfs dos wrappers de linker-driver (`hammer-zig-cc`/`-cxx`) que envuelven a
+/// `zig cc`/`zig c++`. Su ÚNICO efecto es QUITAR `-static` cuando el link también lleva `-shared`.
+///
+/// El harness inyecta `LDFLAGS=-static` para las recetas `link=static` (binario monolítico que pide
+/// la hidratación primaria). Pero meson/autotools aplican ese LDFLAGS a TODOS los targets por igual,
+/// incluidas las extensiones Python (`python.extension_module` → `shared_module`): un `.so` que el
+/// intérprete del sandbox debe `dlopen`. `-static` + `-shared` es contradictorio ⇒ el `.so` sale
+/// ET_EXEC y `dlopen` da `Exec format error` (el muro de gobject-introspection: `_giscanner.so`).
+///
+/// Quitar `-static` SÓLO cuando hay `-shared` es correcto por construcción: un objeto compartido
+/// nunca debe ser estático. Para todo link normal (ejecutable/`.a`) el wrapper es transparente ⇒
+/// byte-idéntico a `zig cc`. El ArtifactHash se deriva de inputs (receta+deps), no de los bytes ⇒
+/// este cambio NO re-hashea nada sellado; sólo destraba builds nuevos con extensiones dinámicas.
+fn write_cc_wrappers(rootfs: &Path) -> hammer_core::Result<()> {
+ use std::os::unix::fs::PermissionsExt;
+ let bindir = rootfs.join("usr/local/bin");
+ std::fs::create_dir_all(&bindir).map_err(|e| {
+ hammer_core::Error::Other(anyhow::anyhow!("no pude crear {}: {e}", bindir.display()))
+ })?;
+ // `case " $* "` detecta `-shared` como palabra completa; el bucle rota los positional params
+ // (pop del frente, push al fondo salvo `-static`) ⇒ filtra preservando orden y seguro con espacios.
+ let body = |driver: &str| {
+ format!(
+ "#!/bin/sh\n\
+ case \" $* \" in\n\
+ \x20 *\" -shared \"*)\n\
+ \x20 n=$#\n\
+ \x20 while [ \"$n\" -gt 0 ]; do\n\
+ \x20 arg=$1; shift\n\
+ \x20 [ \"$arg\" = \"-static\" ] || set -- \"$@\" \"$arg\"\n\
+ \x20 n=$((n - 1))\n\
+ \x20 done\n\
+ \x20 ;;\n\
+ esac\n\
+ exec {driver} -mcpu=baseline \"$@\"\n"
+ )
+ };
+ for (name, driver) in [(CC_WRAPPER, "zig cc"), (CXX_WRAPPER, "zig c++")] {
+ let path = bindir.join(name);
+ std::fs::write(&path, body(driver)).map_err(|e| {
+ hammer_core::Error::Other(anyhow::anyhow!("no pude escribir {}: {e}", path.display()))
+ })?;
+ std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).map_err(|e| {
+ hammer_core::Error::Other(anyhow::anyhow!("no pude chmod {}: {e}", path.display()))
+ })?;
+ }
Ok(())
}