diff --git a/crates/hammer-build/src/sandbox.rs b/crates/hammer-build/src/sandbox.rs index 3bdc9fed..c0f5f8e2 100644 --- a/crates/hammer-build/src/sandbox.rs +++ b/crates/hammer-build/src/sandbox.rs @@ -530,31 +530,40 @@ 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`. +/// `zig cc`/`zig c++`. Su ÚNICO efecto es QUITAR `-static` cuando el link REQUIERE enlace dinámico. /// /// 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`). +/// la hidratación primaria). Pero meson/autotools/giscanner aplican ese LDFLAGS a TODOS los links por +/// igual, incluidos algunos que son INTRÍNSECAMENTE dinámicos y con los que `-static` es +/// contradictorio (`error: using shared libraries requires dynamic linking`). El caso que lo destapó +/// es gobject-introspection, dinámico por naturaleza en TRES puntos: +/// 1. `_giscanner.cpython-*.so`: extensión Python (`shared_module`) que el intérprete `dlopen`ea. +/// Con `-static` sale ET_EXEC ⇒ `dlopen` da `Exec format error`. +/// 2. las tools (`g-ir-compiler`/`-generate`/`-inspect`) linkean la `libgirepository-1.0.so`. +/// 3. el "dumper" del scanner usa `-Wl,--export-dynamic` (necesita la symtab dinámica para +/// introspectar GTypes en runtime) — incompatible con `-static`. /// -/// 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. +/// Regla principista: **`-static` cede ante cualquier link que requiera dinámico** — `-shared`, +/// `--export-dynamic`/`-rdynamic`, o un objeto compartido (`.so`/`.so.N`) en la línea. En todos esos +/// casos `-static` no puede sostenerse; quitarlo es correcto por construcción. Para el link normal +/// (ejecutable estático / `.a`) el wrapper es TRANSPARENTE ⇒ byte-idéntico a `zig cc`, y el resto del +/// árbol sigue estático. 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 enlace dinámico irreducible. 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. + // `case " $* "` detecta como palabra los marcadores de "este link es dinámico": -shared, + // -rdynamic, --export-dynamic, o un shared object en la línea (`…​.so ` final o `…​.so.N` versionado). + // 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 *\" -shared \"* | *\" -rdynamic \"* | *\"--export-dynamic\"* | *\".so \"* | *\".so.\"*)\n\ \x20 n=$#\n\ \x20 while [ \"$n\" -gt 0 ]; do\n\ \x20 arg=$1; shift\n\