# 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 /usr/lib ⇒ libexpat.a (¡sólo estática!) # ls /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=OFF`, Y ME CORRIJO: ESCRIBÍ LO CONTRARIO SOBRE UNA PREMISA FALSA ══════ # Puse `ON` argumentando que era seguro «porque `swayimg` es un programa en C, así que el runtime de # C++ vive entero dentro de la biblioteca y ningún objeto de C++ cruza el límite». **swayimg 5.6 es # C++**, no C (compila `.cpp`), así que la premisa era falsa. Y el cruce no es teórico — está en # `src/imageformat.cpp:124-150`: # # Exiv2::Image::UniquePtr exiv2 = Exiv2::ImageFactory::open(...); // creado en la .so… # } // …y DESTRUIDO en swayimg # catch (Exiv2::Error&) { // lanzada en la .so, capturada acá # # Un `std::unique_ptr` construido en un módulo y destruido en otro libera con el `operator delete` # del consumidor memoria que asignó el `new` de la biblioteca, y la excepción cruza dos copias # ESTÁTICAS de libc++abi. Es la misma figura que segfaulteó `libime_pinyindict` # ([[cuatro-libcxx-en-un-proceso]]), con el agravante de que **swayimg no se puede probar en el # build** —necesita un display Wayland—, así que «sella» no diría nada. # # ⇒ Estática: el código de exiv2 entra en el binario de swayimg y hay **un solo** runtime de C++ en # el proceso. `CMAKE_POSITION_INDEPENDENT_CODE=ON` para que la `.a` sirva también si algún día la # necesita una compartida. 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=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=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.a' 2>/dev/null | head -1) [ -n "$so" ] || { echo "exiv2: no quedó libexiv2.a — NO sello" >&2; find /out -name 'libexiv2*' >&2; exit 1; } # PIC, que es lo que permitiría meterla en una compartida el día que haga falta. readelf -r "$so" 2>/dev/null | grep -qE 'R_X86_64_(GOTPCREL|REX_GOTP|PLT32)' || { echo "exiv2: libexiv2.a no parece PIC" >&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 FALLÓ DOS VECES, DE LAS DOS FORMAS OPUESTAS. Vale anotar las dos.** # # 1. Con la biblioteca COMPARTIDA aceptaba «`libexpat` en NEEDED **o** el símbolo `XML_Parse` # existe», y las dos ramas daban verde sobre un fallo real: el NEEDED estaba ahí **y era # colgante** (apuntaba a la `libexpat.so` del LAB, que no viaja), y # `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.* # # 2. Al pasar a ESTÁTICA lo endurecí exigiendo que `XML_Parse` estuviera **definido dentro** de la # biblioteca… y eso rechaza un build correcto: **una `.a` no contiene a sus deps.** Es una # colección de objetos propios; quien enlaza expat es el CONSUMIDOR. El guardián pedía algo que # no debe estar. # # ⇒ El contrato de una estática no son sus símbolos ajenos: es **lo que su `.pc` le dice al # consumidor que tiene que enlazar**. Si `Requires.private` no nombra expat/brotli/zlib, el # consumidor no lo sabrá, y el fallo saldrá en ÉL con once `undefined symbol: XML_*` que no mencionan # a exiv2 — pasó exactamente así con `libfontconfig.a` y costó dos vueltas de diagnóstico. # # Y la prueba de que las perillas se materializaron pasa a ser la CLI: se enlaza en este mismo build # con expat y brotli de verdad, así que si no entraran, no habría binario que arrancara. for req in expat brotli zlib; do grep -E '^Requires\.private' "$pc" | grep -q "$req" || { echo "exiv2: exiv2.pc no declara '$req' en Requires.private — el consumidor no sabría que debe enlazarlo, y su fallo serían símbolos sin resolver que NO mencionan a exiv2" >&2 grep -E '^Requires|^Libs' "$pc" >&2 exit 1 } done # Y los símbolos PROPIOS de exiv2 sí tienen que estar en la `.a`: es lo único que ella aporta. readelf -sW "$so" 2>/dev/null | grep -q 'ImageFactory' || { echo "exiv2: libexiv2.a no trae ningún símbolo de Exiv2::ImageFactory — es la API que usa el consumidor" >&2; exit 1; } # 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"]