exiv2 + un falso verde del vigía de sonames
exiv2 0.28.9 entra porque sin EXIF una cámara de móvil no rota los píxeles,
anota la orientación en una etiqueta — y un visor que no la lee muestra TODAS
las fotos verticales tumbadas, sin error y sin remedio. Cero deps nuevas: zlib,
brotli y expat ya estaban.
⚠ Y SELLÓ ROTO EN EL PRIMER INTENTO, de una forma que no se ve en el log:
readelf -d libexiv2.so ⇒ NEEDED libexpat.so.1 · NEEDED libz.so.1
corpus expat ⇒ sólo libexpat.a · corpus zlib ⇒ sólo libz.a
.dev-fs/alpine/usr/lib ⇒ libexpat.so.1 · libz.so.1 ← de ahí salieron
Enlazó contra las compartidas DEL LAB, que no entra en hash_inputs ni viaja en
ninguna imagen: artefacto sellado, reproducible, y que no habría cargado. Lo
cazó el CONSUMIDOR —swayimg, con once `undefined symbol: XML_*`—, no el
guardián.
Y lo que hace al fallo difícil de ver: la MISMA receta enlazó bien una dep y mal
otra. brotli entró estática y correcta porque el lab no tiene libbrotli*.so;
expat y zlib se fueron al lab porque sí las tiene. El resultado depende de lo que
la imagen base tenga encima.
⚠ MI GUARDIÁN DIO VERDE POR LAS DOS RAMAS A LA VEZ: aceptaba «libexpat aparece
en NEEDED O el símbolo XML_Parse existe», y el NEEDED estaba ahí Y era colgante,
mientras `readelf -s | grep XML_Parse` casa igual con una entrada UND que con una
definida. Un símbolo presente no es un símbolo resuelto. Ahora exige símbolos
DEFINIDOS (sección != UND) y RECHAZA cualquier NEEDED hacia una dep que el
corpus sólo publica estática.
Arreglado pineando las .a por ruta explícita; verificado que son PIC (217 y 270
relocaciones GOT/PLT), que es lo que les permite entrar en una .so. Resultado:
libexiv2.so con NEEDED libc.so y nada más, XML_Parse y BrotliDecoderDecompress
definidos en código.
⚠ ESTA CLASE YA ESTABA INSTRUMENTADA, y ahí está el segundo hallazgo: existe
`vigia-sonames.py` justamente para esto, con las MISMAS bibliotecas documentadas
(zlib/expat/libffi resolviéndose contra el sysroot del lab). No descubrí una
clase nueva: la volví a pisar con una receta nueva, y el vigía no la vio porque
sólo mira lo DECLARADO en una imagen — un artefacto sellado y sin declarar queda
fuera de su alcance. Ahí es donde sirve el guardián de la receta.
Pero al correrlo encontré algo peor: invocado como `STORE=/store …` (la variable
que usa build-state.py) caía a su default <repo>/store, que está VACÍO en un
clon, no encontraba un artefacto, y reportaba los DIEZ perfiles con «0 sonames
provistos · 0 sin proveedor» — que se lee como limpio. Un guardián que no puede
fallar tapa justo lo que vigila.
· ahora acepta HAMMER_STORE (21 scripts), TAKANA_STORE y STORE (1 script):
el repo quedó partido tras el renombre y unificarlo es su propia unidad
· y ABORTA si el store no tiene artefactos, porque cero nunca es legítimo acá
Con la variable correcta las imágenes están genuinamente limpias: 2192 sonames
en escritorio-kde, 0 sin proveedor en los diez perfiles.
Delta semántico
Ninguna definición cambió: el cambio es de texto, documentación o datos.
2 archivos · +196 −1
- recipes/exiv2.toml +173 −0
- scripts/vigia-sonames.py +23 −1
diff --git a/recipes/exiv2.toml b/recipes/exiv2.toml
new file mode 100644
index 00000000..27e3b293
--- /dev/null
+++ b/recipes/exiv2.toml
@@ -0,0 +1,173 @@
+# exiv2 0.28.9 — metadatos de imagen (EXIF, IPTC, XMP). Entra por una razón concreta y visible.
+#
+# ══ POR QUÉ NO ES UN ADORNO: SIN ESTO LAS FOTOS SALEN GIRADAS ══════════════════════════════════
+# Una cámara de móvil casi nunca rota los píxeles: guarda la imagen como la leyó el sensor y anota
+# la orientación en una etiqueta EXIF. Un visor que no lee EXIF muestra **todas las fotos verticales
+# tumbadas**, sin ningún error y sin que el usuario pueda hacer nada. Es la clase de fallo que no
+# aparece en ninguna métrica del repo y que cualquiera nota en el primer segundo.
+#
+# ⇒ Entra como dep de `swayimg` (el visor), cuya opción `exif` la exige por pkg-config.
+#
+# ══ LO QUE CUESTA: NADA NUEVO ══════════════════════════════════════════════════════════════════
+# Medido sobre `cmake/findDependencies.cmake`, no sobre los paquetes de otra distro:
+# · `ZLIB` y `Brotli` REQUIRED — los dos están en el corpus
+# · `EXPAT` REQUIRED **sólo si** XMP está encendido; está en el corpus
+# · `Filesystem` es `std::filesystem` de C++17, que la libc++ de zig provee
+# · `CURL` sólo con `EXIV2_ENABLE_WEBREADY`, que se apaga
+# · `Intl` sólo con NLS, que se apaga
+# · `Iconv` `find_package` sin REQUIRED ⇒ opcional de verdad (musl trae iconv)
+# · `inih` ⚠ **ME EQUIVOQUÉ ACÁ**: leí `find_package(inih)` sin REQUIRED y lo conté
+# como opcional. Su `cmake/Findinih.cmake:28` hace
+# `message(FATAL_ERROR "inih library not found")` **por su cuenta**, así que
+# corta igual. Leí la LLAMADA y no el MÓDULO — la misma trampa que este repo
+# documenta para `find_library` sin REQUIRED, donde la cura es leer el
+# `else()`. Lo que sí hay es un guard una línea arriba
+# (`if(EXIV2_ENABLE_INIH)`), y por eso se apaga explícitamente.
+# ⇒ cero recetas nuevas.
+#
+# ══ LAS PERILLAS ═══════════════════════════════════════════════════════════════════════════════
+# EXIV2_ENABLE_XMP=ON XMP es lo que escriben Lightroom, darktable y el propio Android; sin
+# esto se leen EXIF e IPTC y se ignora la mitad de los metadatos reales
+# EXIV2_ENABLE_BROTLI=ON JPEG XL y algunos contenedores comprimen metadatos con brotli
+# EXIV2_ENABLE_WEBREADY=OFF leer metadatos por HTTP desde la biblioteca. Una biblioteca de imagen
+# que abre sockets es superficie de ataque que nadie pidió
+# EXIV2_ENABLE_NLS=OFF evita la cadena de gettext, cuyo `msgfmt` en este lab tiene huecos
+# conocidos (ver `recipes/brltty.toml`)
+# EXIV2_BUILD_EXIV2_COMMAND=ON la CLI `exiv2`: es la única forma de comprobar a mano qué
+# metadatos lee un fichero, sin abrir un visor
+# EXIV2_ENABLE_INIH=OFF lo que se pierde es LEER UN `exiv2.ini` con nombres de objetivos
+# fotográficos personalizados — exiv2 sigue reconociendo los objetivos
+# que trae en sus tablas internas. Es proporcionado frente a meter dos
+# recetas (`inih` publica una biblioteca C y un `INIReader` de C++, y su
+# Find exige las dos)
+#
+# ══ ⚠⚠ LAS ESTÁTICAS SE PINEAN A MANO, Y ESTO ES EL HALLAZGO IMPORTANTE DE ESTA RECETA ═════════
+# La primera versión selló un artefacto que **no habría cargado**, y el motivo no se ve en ningún
+# log del build:
+#
+# readelf -d libexiv2.so ⇒ NEEDED libexpat.so.1 · NEEDED libz.so.1
+# ls <artefacto expat>/usr/lib ⇒ libexpat.a (¡sólo estática!)
+# ls <artefacto zlib>/usr/lib ⇒ libz.a (¡sólo estática!)
+# ls .dev-fs/alpine/usr/lib ⇒ libexpat.so.1 · libz.so.1 ← de acá salieron
+#
+# O sea que cmake enlazó contra las compartidas **DEL LAB**. El artefacto sella, reproduce, y en una
+# imagen el cargador no encontraría ni `libexpat.so.1` ni `libz.so.1`, porque el lab no viaja.
+# Es la familia del «NEEDED colgante»: sella, reproduce y no corre.
+#
+# ⇒ Se pinean las `.a` del corpus por ruta explícita. Se comprobó que **son PIC** (217 y 270
+# relocaciones GOT/PLT), que es lo que permite que entren en una `.so`. El repo también tiene
+# `expat-shared` y `zlib-shared` para este caso; se eligió la estática porque deja el artefacto
+# **autosuficiente y verificable desde dentro del sandbox**: con la compartida el `NEEDED` sería
+# legítimo, pero el lab está montado igual y desde dentro **no hay forma de distinguir** si la `.so`
+# vino del corpus o de la imagen.
+#
+# ⚠ Y `brotli` no hizo falta pinearlo: no hay `libbrotli*.so` en el lab, así que su `.a` entró sola.
+# **Eso es exactamente lo que hace al fallo tan difícil de ver** — la misma receta enlaza bien una
+# dep y mal otra, según lo que el lab tenga encima.
+#
+# ⚠ `-Wno-error=date-time`: `src/version.cpp:431-432` usa `__DATE__`/`__TIME__` y el lab compila
+# con `-Werror=date-time`, su política de reproducibilidad. Desactivarlo es seguro porque el sandbox
+# exporta `SOURCE_DATE_EPOCH=1` y con eso clang hace que esos macros rindan la fecha fija en vez de
+# la del reloj, así que el binario sigue determinista. Mismo razonamiento que `fastfetch`, `cairo`,
+# `appstream` y `brltty` — **cinco recetas con el mismo parche** ya empieza a parecer un caso para
+# decidir en el lab y no receta por receta, pero eso es su propia unidad de trabajo.
+#
+# ⚠ `BUILD_SHARED_LIBS=ON` y es SEGURO acá, a diferencia de `libime`: `swayimg` es un programa en C
+# que enlaza `libexiv2.so`, así que el runtime de C++ vive ENTERO dentro de la biblioteca y ningún
+# objeto de C++ cruza el límite de módulo. El problema de las cuatro copias de libc++ aparece cuando
+# un `std::` creado en un módulo se usa en otro — acá no pasa.
+name = "exiv2"
+version = "0.28.9"
+license = "GPL-2.0-or-later"
+
+[source]
+tarball = "https://github.com/Exiv2/exiv2/archive/refs/tags/v0.28.9/exiv2-0.28.9.tar.gz"
+sha256 = "700b76b97695b2fab4ef8c79619c68ae57d09e0c130724791cafbd39e0eb4aef"
+
+[build]
+compiler = "zig-cc"
+target = "x86_64-linux-musl"
+link = "dynamic"
+flags = []
+
+[build.phases]
+configure = '''
+ZW="$PWD/.zwrap"; mkdir -p "$ZW"
+printf '%s\n' '#!/bin/sh' 'for a do' 'case "$a" in --target=*) a=--target=x86_64-linux-musl ;; esac' 'set -- "$@" "$a"' 'shift' 'done' 'exec /opt/zig/zig cc -mcpu=baseline "$@"' > "$ZW/cc"
+printf '%s\n' '#!/bin/sh' 'for a do' 'case "$a" in --target=*) a=--target=x86_64-linux-musl ;; esac' 'set -- "$@" "$a"' 'shift' 'done' 'exec /opt/zig/zig c++ -mcpu=baseline "$@"' > "$ZW/cxx"
+chmod +x "$ZW/cc" "$ZW/cxx"
+# `rm -rf build`: un build/ fallido sobrevive en el árbol reutilizado y el caché de cmake hace que
+# el reintento repita el error viejo en dos segundos.
+rm -rf build
+cmake -B build -G Ninja -Wno-dev \
+ -DCMAKE_C_COMPILER="$ZW/cc" -DCMAKE_CXX_COMPILER="$ZW/cxx" \
+ -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr \
+ -DCMAKE_INSTALL_LIBDIR=lib \
+ -DPKG_CONFIG_ARGN=--static \
+ -DCMAKE_C_FLAGS=-Wno-error=date-time -DCMAKE_CXX_FLAGS=-Wno-error=date-time \
+ -DBUILD_SHARED_LIBS=ON \
+ -DEXIV2_ENABLE_XMP=ON -DEXIV2_ENABLE_BROTLI=ON \
+ -DEXIV2_ENABLE_WEBREADY=OFF -DEXIV2_ENABLE_NLS=OFF \
+ -DEXIV2_ENABLE_BMFF=ON \
+ -DEXIV2_ENABLE_INIH=OFF \
+ -DEXPAT_LIBRARY=/usr/lib/libexpat.a -DEXPAT_INCLUDE_DIR=/usr/include \
+ -DZLIB_LIBRARY=/usr/lib/libz.a -DZLIB_INCLUDE_DIR=/usr/include \
+ -DEXIV2_BUILD_EXIV2_COMMAND=ON -DEXIV2_BUILD_SAMPLES=OFF -DEXIV2_BUILD_UNIT_TESTS=OFF
+'''
+compile = "cmake --build build"
+# El guardián comprueba que las PERILLAS se hayan materializado, no sólo que haya ficheros. Una
+# exiv2 construida sin XMP o sin brotli se sella igual, exporta la misma API, y devuelve «no hay
+# metadatos» sobre ficheros que sí los tienen — un fallo que se lee como «la foto no trae EXIF».
+# La prueba es el NEEDED de la compartida, que es donde se ve si la dependencia entró de verdad.
+install = '''
+set -e
+DESTDIR=/out cmake --install build
+so=$(find /out/usr/lib -name 'libexiv2.so*' -type f 2>/dev/null | head -1)
+[ -n "$so" ] || { echo "exiv2: no quedó libexiv2.so — swayimg la enlaza" >&2; find /out -name 'libexiv2*' >&2; exit 1; }
+[ -f /out/usr/include/exiv2/exiv2.hpp ] || { echo "exiv2: falta include/exiv2/exiv2.hpp" >&2; exit 1; }
+pc=$(find /out/usr/lib -name 'exiv2.pc' 2>/dev/null | head -1)
+[ -n "$pc" ] || { echo "exiv2: falta exiv2.pc — swayimg la busca por pkg-config (dependency('exiv2'))" >&2; exit 1; }
+[ -x /out/usr/bin/exiv2 ] || { echo "exiv2: falta la CLI /usr/bin/exiv2 — es la única forma de inspeccionar metadatos a mano" >&2; exit 1; }
+
+# XMP necesita expat y brotli necesita libbrotlidec: si las perillas no se materializaron, no
+# aparecen en NEEDED. `.a` del corpus no declararían NEEDED, así que se admiten las dos formas:
+# o el soname en NEEDED, o el símbolo exportado si entró estático.
+# ⚠ **ESTE CONTROL ESTUVO MAL Y DEJÓ PASAR EL ARTEFACTO ROTO.** Mi versión anterior aceptaba
+# «`libexpat` aparece en NEEDED **o** el símbolo `XML_Parse` existe en la tabla», y las dos ramas
+# daban verde sobre el fallo: el NEEDED estaba ahí **y era colgante**, y `readelf -s | grep XML_Parse`
+# casa igual con una entrada **UND** (sin definir) que con una definida.
+#
+# ⇒ Ahora se exige que los símbolos estén **DEFINIDOS** —columna de sección distinta de `UND`— y
+# que NO quede ningún NEEDED hacia una biblioteca que el corpus sólo publica estática. Un símbolo
+# presente no es un símbolo resuelto.
+definido() {
+ readelf -sW "$1" 2>/dev/null | awk -v s="$2" '$8==s && $7!="UND" {found=1} END{exit !found}'
+}
+definido "$so" XML_Parse || {
+ echo "exiv2: XML_Parse no está DEFINIDO en la compartida — expat no entró (o entró como referencia colgante) y el XMP no funcionaría" >&2
+ readelf -sW "$so" 2>/dev/null | awk '/XML_Parse/{print " "$7, $8}' >&2
+ exit 1
+}
+definido "$so" BrotliDecoderDecompress || definido "$so" BrotliDecoderCreateInstance || {
+ echo "exiv2: ningún símbolo BrotliDecoder* DEFINIDO — los metadatos comprimidos no se leerían" >&2
+ exit 1
+}
+# Y el control que faltaba: NINGÚN NEEDED hacia lo que el corpus sólo tiene estático. Si aparece,
+# salió del LAB y la imagen no lo tendrá.
+for mal in libexpat libz.so libbrotli; do
+ readelf -d "$so" 2>/dev/null | grep NEEDED | grep -q "$mal" && {
+ echo "exiv2: NEEDED hacia $mal — el corpus sólo publica esa dep ESTÁTICA, así que esa .so salió del lab y no viajaría en ninguna imagen" >&2
+ readelf -d "$so" 2>/dev/null | grep NEEDED >&2
+ exit 1
+ }
+done
+# Y que la CLI ARRANQUE: es C++ y una ruta mal resuelta la deja muerta aunque el fichero exista.
+# Ejecución y truncado SEPARADOS — el estado de una tubería es el del último comando, y eso ya me
+# selló un fallo hoy (ver `recipes/automake.toml`).
+sal=$(/out/usr/bin/exiv2 --version 2>&1) || { echo "exiv2: la CLI no arranca" >&2; printf '%s\n' "$sal" >&2; exit 1; }
+printf '%s\n' "$sal" | head -1 | grep -qE '^exiv2 0\.28\.9' || { echo "exiv2: --version inesperado: $(printf '%s\n' "$sal" | head -1)" >&2; exit 1; }
+echo "exiv2: $(basename "$so") + CLI + exiv2.pc · xmp y brotli presentes"
+'''
+
+[deps]
+build = ["cmake", "samurai", "pkgconf", "zlib", "brotli", "expat", "binutils"]
diff --git a/scripts/vigia-sonames.py b/scripts/vigia-sonames.py
index bc07fe0e..f0fd3ec2 100755
--- a/scripts/vigia-sonames.py
+++ b/scripts/vigia-sonames.py
@@ -50,7 +50,29 @@ import yupana
import targets as targets_mod
ROOT = yupana.ROOT
-STORE = os.environ.get("HAMMER_STORE", str(ROOT / "store"))
+
+# ⚠ **UN VIGÍA QUE ESCANEA CERO ARTEFACTOS DECÍA «0 SIN PROVEEDOR» — o sea, limpio.**
+# Medido el 2026-10-20: invocado como `STORE=/store …` (la variable que usa `build-state.py`) este
+# script caía a su default `<repo>/store`, que **está VACÍO en un clon** —el store de verdad está en
+# `/store`—, no encontraba un solo artefacto, y reportaba los DIEZ perfiles con «0 sonames provistos
+# · 0 sin proveedor». Un guardián que no puede fallar es peor que no tenerlo: tapa justo lo que
+# vigila. Con la variable correcta el mismo comando da 2192 sonames en KDE y 0 sin proveedor.
+#
+# Dos cosas, entonces:
+# 1. **Se aceptan los tres nombres.** El repo está partido tras el renombre: 21 scripts usan
+# `HAMMER_STORE` y `build-state.py` usa `STORE`. Unificarlos es su propia unidad de trabajo;
+# mientras eso no pase, que el vigía entienda los dos (más `TAKANA_STORE`) evita el falso verde.
+# 2. **Se ABORTA si el store no tiene artefactos.** Cero artefactos nunca es un resultado
+# legítimo para este vigía: es una invocación mal hecha, y debe verse como error y no como
+# éxito.
+STORE = (os.environ.get("HAMMER_STORE") or os.environ.get("TAKANA_STORE")
+ or os.environ.get("STORE") or str(ROOT / "store"))
+_n_art = len(glob.glob(os.path.join(STORE, "*-*")))
+if _n_art == 0:
+ sys.exit("vigia-sonames: el store '%s' no tiene ningún artefacto.\n"
+ " Con un store vacío este vigía diría «0 sin proveedor» en TODOS los perfiles, que se\n"
+ " lee como limpio y no lo es. Indicá el store de verdad:\n"
+ " HAMMER_STORE=/store python3 scripts/vigia-sonames.py --imagen" % STORE)
HAMMER = str(ROOT / "target/release/takana")
_cache = {}