gioser

sergio/takana

> 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…

paso 8 ter: lld21 sobre zlib-rs — el caso dinámico destapó que la .so no declaraba SONAME

pigz probó el enlace estático; faltaba el que falla callado. lld enlaza libz.so.1 DINÁMICAMENTE, así
que acá no se prueba compilar: se prueba qué NEEDED queda grabado y si el cargador lo resuelve.

EL DEFECTO, que el caso estático no podía mostrar: la .so que sale de cargo NO declara SONAME
(readelf -d: sólo NEEDED), mientras la zlib-shared del corpus declara libz.so.1. Un consumidor
enlazado contra una .so sin soname graba la RUTA con la que se enlazó, no el nombre ⇒ sella,
reproduce y no corre. Arreglado en recipes/zlib-rs.toml con -C link-arg=-Wl,-soname,libz.so.1, donde
el hash lo cubre. `yupana radio zlib-rs` dio 1 dependiente (pigz-zlibrs) y se reconstruyó en el acto.

Con el soname puesto el swap es transparente: b3:45eed481… sellada, NEEDED idénticos a los del lld
canónico (libz.so.1, libgcc_s.so.1, libc.so), binario de 96.628.672 B contra 96.628.688, y `--version`
correcto cargando cada uno su libz.

⚠ En el caso dinámico NO hace falta -lunwind: la .so trae NEEDED libgcc_s.so.1, que provee los
_Unwind_* en ejecución. La reserva del unwinder era del enlace estático.

PRUEBA FUNCIONAL, no un --version: lld sólo usa zlib para las secciones de depuración comprimidas
(por eso su receta enciende LLVM_ENABLE_ZLIB, tras un error real con los crt* de rust). Se le dio un
objeto compilado con -gz=zlib y se le pidió comprimir la salida:
  canónico 5312 B · 3 secciones comprimidas · el readelf de binutils la descomprime OK
  zlib-rs  5328 B · 3 secciones comprimidas · el readelf de binutils la descomprime OK
O sea inflate del input y deflate del output dentro de lld, con una TERCERA implementación leyendo el
resultado. Y el sha256 del --debug-dump=info descomprimido es IDÉNTICO entre los dos: zlib-rs no
cambia los datos, cambia su empaquetado.

RECOMENDACIÓN que antes de medir no se podía dar: empezar la campaña por los consumidores DINÁMICOS
—ahí el coste en bytes es una sola librería compartida y el unwinder no hace falta— y dejar los
estáticos, que pagan +2,9 MB cada uno.

6b10a805afc6402d748c99afa6ca907f59c128c6 · by Sergio · (29 d ago) · parent df819187e · browse the tree at this commit

Semantic delta

No definition changed: this change is text, documentation or data.

3 files · +169 −1

diff --git a/docs/plan-que-sigue-tras-busybox.md b/docs/plan-que-sigue-tras-busybox.md
index 7c2812f3..05b3b073 100644
--- a/docs/plan-que-sigue-tras-busybox.md
+++ b/docs/plan-que-sigue-tras-busybox.md
@@ -1053,3 +1053,76 @@ para una imagen con muchos consumidores de zlib, +2,9 MB por binario no es grati
 ⚠ `scripts/yupana.py` gana el sufijo `-zlibrs` en `SUFIJOS_VARIANTE`: sin eso el invariante (c) marca
 `pigz`/`pigz-zlibrs` como «colisión-fuente ACCIONABLE», que es un falso positivo — y un guardián con
 falsos positivos se empieza a ignorar. Comprobado: `yupana duplicados` sigue en «ninguna».
+
+## Paso 8 ter — el consumidor DINÁMICO: `lld21` sobre `zlib-rs`, y el defecto que sólo este caso podía mostrar
+
+`pigz-zlibrs` probó el enlace estático. Faltaba el otro, que es el que falla callado: **lld enlaza
+`libz.so.1` dinámicamente** (`link = "dynamic"` en su receta, por una razón medida que está escrita
+allá), así que acá no se prueba sólo compilar — se prueba **qué `NEEDED` queda grabado y si el
+cargador lo resuelve**. `recipes/lld21-zlibrs.toml`: `b3:45eed481…`, sellada, ~6 min de build (sólo el
+subproyecto lld contra el `llvm21` ya sellado).
+
+### El defecto: la `.so` de zlib-rs NO declaraba SONAME
+
+```
+readelf -d  zlib-rs/usr/lib/libz.so.1   ⇒ NEEDED libgcc_s.so.1, NEEDED libc.so … y NINGÚN SONAME
+readelf -d  zlib-shared/usr/lib/libz.so.1 ⇒ SONAME libz.so.1
+```
+
+Un consumidor enlazado contra una `.so` sin soname graba en su `NEEDED` **la ruta con la que se
+enlazó**, no el nombre de la librería ⇒ el binario sella, reproduce y **no corre**
+([[needed-colgante-libstdcxx]]). El caso estático no podía mostrarlo: ahí el `.a` entra al binario y
+no hay `NEEDED` que grabar.
+
+Arreglado en `recipes/zlib-rs.toml` con `-C link-arg=-Wl,-soname,libz.so.1` —en el compile, donde el
+hash lo cubre—, y re-sellado. `yupana radio zlib-rs` dijo **1 dependiente** (pigz-zlibrs), que se
+reconstruyó en el acto: la disciplina de medir el radio antes de tocar, aplicada a mi propio cambio.
+
+### Con el soname puesto, el swap es transparente
+
+| | `lld21` (zlib 1.3.1) | `lld21-zlibrs` |
+|---|---|---|
+| `NEEDED` | `libz.so.1` · `libgcc_s.so.1` · `libc.so` | **idénticos** |
+| binario | 96.628.688 B | 96.628.672 B |
+| `--version` con su libz cargada | `LLD 21.1.2` | `LLD 21.1.2` |
+
+⚠ **Y en el caso dinámico NO hace falta `-lunwind`**: la `.so` de zlib-rs trae `NEEDED libgcc_s.so.1`,
+que es quien provee los `_Unwind_*` en ejecución. La reserva del unwinder era **del enlace estático**.
+
+### La prueba funcional: lld leyendo Y escribiendo `ELFCOMPRESS_ZLIB`
+
+No alcanza un `--version`: lld sólo usa zlib para las secciones de depuración comprimidas (por eso su
+receta enciende `LLVM_ENABLE_ZLIB`, tras un error real con los `crt*` de rust). Así que se le dio de
+comer exactamente eso — un objeto compilado con `-gz=zlib` — y se le pidió comprimir la salida:
+
+```
+lld -flavor gnu -r cz.o -o r.o --compress-debug-sections=zlib
+  canónico  salida 5312 B · 3 secciones comprimidas · el readelf de binutils la DESCOMPRIME OK
+  zlib-rs   salida 5328 B · 3 secciones comprimidas · el readelf de binutils la DESCOMPRIME OK
+```
+
+O sea: **inflate del input y deflate del output, los dos dentro de lld**, y una TERCERA
+implementación (el `readelf` de binutils) lee el resultado. Y el dato más fino:
+
+```
+sha256 del `--debug-dump=info` descomprimido:  canónico == zlib-rs
+```
+
+**El contenido descomprimido es idéntico; lo único que difiere es el stream deflate** (+16 B). Que es
+la reserva nº 1 dicha con precisión: zlib-rs no cambia los datos, cambia su empaquetado.
+
+### El veredicto, cerrado
+
+Los dos casos de enlace están probados con consumidores reales del corpus, sin tocar una línea de sus
+fuentes. Lo que queda para decidir la campaña de las 122 no es técnico sino de política:
+
+1. **La salida comprimida no es byte-idéntica** ⇒ los artefactos que embeben datos comprimidos cambian
+   de contenido. Es determinista y el contenido descomprimido es el mismo, pero el `ArtifactHash`
+   cambia y con él lo que el usuario descarga.
+2. **`gzprintf` no existe** (pide nightly) ⇒ hay que mirar receta por receta.
+3. **El tamaño**: +2,9 MB en el binario estático de pigz; en el dinámico, **cero** (la `.so` es 1,7 M
+   contra 108 K de la de zlib, y se comparte).
+
+⇒ Y de ahí sale la recomendación concreta, que antes de medir no se podía dar: **empezar por los
+consumidores DINÁMICOS**, donde el coste en bytes es una sola librería compartida y el unwinder no
+hace falta. Los estáticos pagan 2,9 MB cada uno.
diff --git a/recipes/lld21-zlibrs.toml b/recipes/lld21-zlibrs.toml
new file mode 100644
index 00000000..76191e6f
--- /dev/null
+++ b/recipes/lld21-zlibrs.toml
@@ -0,0 +1,83 @@
+# LLD 21.1.2 sobre zlib-rs — la PRUEBA DE CONSUMIDOR DINÁMICO del paso 8 del plan.
+#
+# ══ POR QUÉ ESTA VARIANTE, Y POR QUÉ lld ES EL CASO QUE FALTABA ════════════════════════════════
+# `pigz-zlibrs` probó el enlace ESTÁTICO (`libz.a` + `-lunwind`). Este prueba el otro, que es el que
+# de verdad puede fallar callado: **lld enlaza `libz.so.1` DINÁMICAMENTE** —`link = "dynamic"` en su
+# receta, por una razón medida que está escrita allá— así que acá lo que se pone a prueba no es sólo
+# compilar, es que el `NEEDED` grabado sea el correcto y que el cargador lo resuelva en ejecución.
+#
+# ⚠ **Y ahí apareció el defecto que el caso estático no podía mostrar: la `.so` de zlib-rs no
+# declaraba SONAME.** `readelf -d` sobre la `libz_rs.so` que sale de cargo mostraba `NEEDED` y ningún
+# `SONAME`, mientras la `zlib-shared` del corpus declara `libz.so.1`. Un consumidor enlazado contra una
+# `.so` sin soname graba la RUTA con la que se enlazó, no el nombre de la librería ⇒ sella, reproduce
+# y no corre ([[needed-colgante-libstdcxx]]). Arreglado en `recipes/zlib-rs.toml` con
+# `-C link-arg=-Wl,-soname,libz.so.1`; esta receta existe para comprobarlo de punta a punta.
+#
+# ⚠ **lld USA zlib de verdad, no de adorno**: `LLVM_ENABLE_ZLIB` está encendido porque los objetos
+# `crt*` del sysroot de rust traen las secciones de depuración comprimidas
+# (`ELFCOMPRESS_ZLIB`) y un enlazador sin zlib no puede ni leerlos. O sea que si zlib-rs no
+# descomprimiera bien, el fallo aparecería enlazando, no en un `--version`.
+#
+# Lo único que cambia frente a `recipes/lld21.toml`: la dep (`zlib-rs` en vez de `zlib`). No hace
+# falta `-lunwind` —y eso es un hallazgo del caso dinámico—: la `.so` de zlib-rs trae
+# `NEEDED libgcc_s.so.1`, que es quien provee los `_Unwind_*` en ejecución, así que el consumidor no
+# tiene que sumar nada. La reserva del unwinder era del enlace ESTÁTICO.
+name    = "lld21-zlibrs"
+version = "21.1.2"
+license = "Apache-2.0 WITH LLVM-exception"
+
+[source]
+# El mismo tarball que `llvm21`, byte por byte: la URL es locator y el sha256 es la identidad
+# (ADR 0013), así que compartir fuente entre dos recetas no cuesta nada.
+tarball = "https://github.com/llvm/llvm-project/releases/download/llvmorg-21.1.2/llvm-project-21.1.2.src.tar.xz"
+sha256  = "1a417d1c8faf8d93e73fec1cbb76d393ed3218974c2283c7bac9672d3d47c54b"
+
+[build]
+compiler = "zig-cc"
+target   = "x86_64-linux-musl"
+# ⚠ `dynamic` NO es una preferencia: con `static` el binario SELLA, arranca y **se muere en cuanto
+# hace algo** (medido 2026-09-16). `ld.lld --version` y `--help` contestan bien; cualquier enlace
+# —incluso uno que sólo debería dar un error, como un fichero de entrada inexistente— termina en
+# SIGSEGV con el banner de LLVM y un «Stack dump:» vacío. La firma es la de los registros estáticos
+# de LLVM: `cl::opt`, `TargetRegistry` y compañía se inicializan desde constructores globales que
+# viven en objetos de las `.a`, y un enlace estático agresivo los DESCARTA por no estar
+# referenciados — lo que funciona es lo que no los necesita (`--version`), y lo que no, revienta.
+# Es la misma familia que `link-static-mentira-libtool`: el flag global de la receta decide cosas
+# que la receta no dice.
+link     = "dynamic"
+strip_debug = true
+
+[deps]
+# `llvm21` NO es un adorno: el build standalone de lld necesita su `lib/cmake/llvm/LLVMConfig.cmake`
+# y sus `.a`. Es la misma dep que el paso `Lld` del bootstrap de rustc habría usado.
+build = ["llvm21", "cmake", "samurai", "python3", "pkgconf", "zlib-rs"]
+
+[build.phases]
+# ⚠ `LLVM_ENABLE_ZLIB` va ENCENDIDO —al revés que en `llvm21`— y lo decidió un error, no el gusto:
+#
+#     lld: error: …/self-contained/crtn.o:(.debug_line) is compressed with ELFCOMPRESS_ZLIB,
+#          but lld is not built with zlib support
+#
+# Los objetos `crt*` que rust trae en su sysroot llevan las secciones de depuración COMPRIMIDAS, así
+# que un enlazador sin zlib no puede ni leerlos. Para `llvm21` apagarlo era gratis; para el
+# ENLAZADOR no lo es.
+#
+# `g++` con los runtimes estáticos, igual que `llvm21` y `cmake`: C++ a escala con zig sigue siendo
+# terreno minado, y además LLD tiene que casar ABI con el LLVM contra el que enlaza — que se
+# construyó exactamente así.
+configure = '''
+CC=gcc CXX='g++ -static-libstdc++ -static-libgcc' \
+cmake -S lld -B build -G Ninja \
+  -DCMAKE_BUILD_TYPE=Release \
+  -DCMAKE_INSTALL_PREFIX=/usr \
+  -DLLVM_DIR=/usr/lib/cmake/llvm \
+  -DLLVM_INCLUDE_TESTS=OFF \
+  -DLLVM_ENABLE_ZSTD=OFF \
+  -DLLVM_ENABLE_LIBXML2=OFF
+'''
+compile = "ninja -C build"
+# ⚠ LOS CINCO `lld` SON EL MISMO BINARIO, Y cmake LOS INSTALA COMO CINCO COPIAS: medido, 5 × 96 M =
+# **478 M de artefacto para 97 M de enlazador**. `lld` despacha por `argv[0]`, así que los cuatro
+# alias son symlinks por diseño de upstream — instalarlos como copias es sólo cómo quedó su cmake.
+# En una caja cuya raíz son 5,8 G, 380 M de duplicado no son un detalle.
+install = "DESTDIR=/out ninja -C build install && cd /out/usr/bin && for f in ld.lld ld64.lld lld-link wasm-ld; do ln -sf lld \"$f\"; done"
diff --git a/recipes/zlib-rs.toml b/recipes/zlib-rs.toml
index e2429445..d317c5b0 100644
--- a/recipes/zlib-rs.toml
+++ b/recipes/zlib-rs.toml
@@ -35,6 +35,13 @@
 # provee. Por eso esta receta **no** se puede declarar como dep y ya: el consumidor tiene que sumar
 # `LIBS=-lunwind` o su equivalente.
 #
+# ⚠ **LA `.so` NO DECLARA SONAME SOLA, y sin eso es inservible para un consumidor dinámico**
+# (medido 2026-09-22): `readelf -d` sobre la `libz_rs.so` que sale de cargo muestra `NEEDED` y ningún
+# `SONAME`, mientras la `zlib-shared` del corpus declara `libz.so.1`. Un consumidor que enlace contra
+# una `.so` sin soname graba en su `NEEDED` **la ruta con la que se enlazó**, no el nombre de la
+# librería ⇒ el binario sella, reproduce y **no corre** ([[needed-colgante-libstdcxx]]). Se arregla
+# con `-C link-arg=-Wl,-soname,libz.so.1` en el compile, que es donde el hash lo cubre.
+#
 # ⚠ **`zlibVersion()` contesta `1.3.0-zlib-rs-0.6.8`.** El chequeo de versión de `deflateInit_` compara
 # sólo el primer carácter, así que pasa; un `configure` que parsee la cadena entera puede no gustarle.
 #
@@ -76,7 +83,12 @@ flags    = []
 
 [build.phases]
 configure = "true"
-compile   = "cargo build --release --locked --offline -p libz-rs-sys-cdylib --features libz-rs-sys-cdylib/gz"
+# `RUSTFLAGS` con el soname: acá es seguro porque este árbol NO tiene proc-macros (el problema
+# documentado de `+crt-static` en RUSTFLAGS) y el único artefacto enlazado es la propia cdylib.
+compile   = '''
+RUSTFLAGS="-C link-arg=-Wl,-soname,libz.so.1" \
+  cargo build --release --locked --offline -p libz-rs-sys-cdylib --features libz-rs-sys-cdylib/gz
+'''
 # Se instala con los NOMBRES de zlib (`libz.a`, `libz.so.1`) y con los headers de zlib-rs, para que un
 # consumidor que hoy pide `-lz` y `#include <zlib.h>` no tenga que cambiar una línea. El `.so.1` lleva
 # el enlace `libz.so` porque es el que busca el enlazador al ver `-lz`.