libime: estática y PIC, porque había CUATRO libc++ en un proceso
libime_pinyindict segfaulteaba con CUALQUIER entrada, incluso un fichero VACÍO,
mientras libime_tabledict y libime_slm_build_binary funcionaban. El diagnóstico
salió del kernel, no de conjeturas — y antes se descartaron dos hipótesis con
dos mediciones baratas: `ulimit -s unlimited` seguía cayendo (no es la pila) y
el pico de RSS era 3 MiB (no es memoria).
segfault at 1064e20 ip 0000000001064e20 … error 15 in libime_pinyindict
ip == la dirección del fallo, y es un fetch de instrucción: el flujo saltó a
algo que no es código. El binario es no-PIE, así que la dirección se resuelve
directo en su tabla de símbolos y cae en .data.rel.ro, exactamente en
0000000001064e20 d typeinfo for std::__1::collate<wchar_t>
Medido: el exe, libIMEPinyin.so, libIMECore.so y libFcitx5Utils.so definen cada
uno ~64 símbolos std::__1::locale:: propios. CUATRO copias de libc++, porque zig
la enlaza estáticamente en cada módulo; el despacho de facetas de std::locale
cruza el límite y devuelve un typeinfo donde se espera una función. Explica el
patrón exacto: slm_build_binary lleva kenlm estático en el exe y funciona;
tabledict usa su .so pero no toca facetas y funciona; pinyindict crea el
ifstream en el exe y lo consume en la .so y revienta.
BUILD_SHARED_LIBS=Off + POSITION_INDEPENDENT_CODE=ON quita el cruce. El PIC no
es opcional: chinese-addons es un addon .so y una .a no-PIC no entra en una
compartida. El guardián se ajustó al artefacto que la receta produce AHORA (.a,
y además comprueba que sean PIC) en vez de seguir pidiendo .so.
⚠ Esto NO cura la duplicación, que sigue en todo el corpus de C++: acá se evita
el cruce. Cualquier cadena donde un objeto con facetas de locale pase de un
ejecutable a una .so puede caer igual, y eso incluye los addons que fcitx5 abre
con dlopen, donde «sella» sólo prueba que construye. La cura de raíz sería una
libc++ compartida y es una campaña propia.
Con estático aparece un límite de upstream: install(EXPORT) incluye IMECore, que
depende de kenlm, y kenlm es una OBJECT library que upstream no exporta porque
nunca construye estático. Parche mínimo y verificado en los dos sentidos: kenlm
pasa a STATIC y entra al mismo export set.
Y la clausura de runtime de las tres recetas de datos, medida recorriendo NEEDED
recursivamente contra el store: libime + fcitx5 + zstd, cero huérfanos. Sin
fcitx5 el cargador no encuentra libFcitx5Utils.so.2 y la fase muere con exit
127, que se lee como «no existe el ejecutable» y es «no existe su biblioteca».
Y en chinese-addons, el parche de pinyinhelper completo: faltaba borrar los
set(PY_*_URL …), y lo descubrió el propio control de salida de la receta, que
abortó diciendo que el fichero «todavía referencia la red tras el parche».
Probado en seco contra la fuente real: 8 coincidencias, cero referencias
restantes.
Verificado: libime sella con 3 .a PIC + las 4 herramientas; libime-dict produce
sc.dict 4,49 MB + extb.dict 156 KB; libime-table, las 8 tablas.
Semantic delta
No definition changed: this change is text, documentation or data.
5 files · +113 −7
- recipes/fcitx5-chinese-addons.toml +8 −0
- recipes/libime-dict.toml +13 −1
- recipes/libime-lm.toml +13 −1
- recipes/libime-table.toml +13 −1
- recipes/libime.toml +66 −4
diff --git a/recipes/fcitx5-chinese-addons.toml b/recipes/fcitx5-chinese-addons.toml
index 866a2575..2124f600 100644
--- a/recipes/fcitx5-chinese-addons.toml
+++ b/recipes/fcitx5-chinese-addons.toml
@@ -95,9 +95,14 @@ import sys, re
p = sys.argv[1]
s = open(p, encoding='utf-8').read()
# Fuera download/extract (bajan por red) y los install de los .mb (los entregan las recetas de datos)
+# ⚠ Los `set(PY_*_URL …)` TAMBIÉN se van, y eso lo descubrió el propio control de salida de esta
+# receta: quité las llamadas y dejé las URLs, así que el `grep` final denunció que el fichero
+# «todavía referencia la red tras el parche» y abortó. Hizo exactamente lo que tenía que hacer —
+# un parche a medias habría sellado un artefacto que no baja nada por casualidad, no por diseño.
patrones = [
r'fcitx5_download\([^)]*\)\n',
r'fcitx5_extract\([^)]*\)\n',
+ r'set\(PY_(?:STROKE|TABLE)_URL\s*[^)]*\)\n',
r'install\(FILES "\$\{PY_(?:STROKE|TABLE)_TGT\}"[^)]*\)\n',
]
for pat in patrones:
@@ -150,6 +155,9 @@ echo "fcitx5-chinese-addons: $n addon(s), chttrans enlazado a opencc, chaizi.dic
[deps]
build = ["cmake", "samurai", "extra-cmake-modules", "pkgconf", "fcitx5", "libime", "boost",
"opencc", "zstd", "gettext-tiny", "libxkbcommon", "wayland", "wayland-protocols",
+ # `python3`: lo usa el PARCHE de pinyinhelper de la fase configure, no el build de upstream.
+ # Sin declararlo la fase muere con exit 127 y el error no nombra a python.
+ "python3",
"glib", "pcre2", "libffi", "expat", "zlib", "dbus", "binutils"]
# ⚠ Datos que el addon busca EN EJECUCIÓN, no al construir. Van en `runtime` precisamente por eso:
# no entran en el hash del build —`runtime` no re-hashea— pero sí en la clausura de la imagen, que
diff --git a/recipes/libime-dict.toml b/recipes/libime-dict.toml
index fec96eee..401668f2 100644
--- a/recipes/libime-dict.toml
+++ b/recipes/libime-dict.toml
@@ -64,4 +64,16 @@ echo "libime-dict: sc.dict $(stat -c%s /out/usr/share/libime/sc.dict) B + extb.d
'''
[deps]
-build = ["libime"]
+# ⚠ **`fcitx5` y `zstd` NO son opcionales acá, y el fallo NO dice que falten.** Las herramientas de
+# libime son binarios DINÁMICOS: `libime_pinyindict` declara
+# `NEEDED libIMEPinyin.so.0 libIMECore.so.0 libFcitx5Utils.so.2`. El sandbox sólo monta lo
+# declarado, así que con `libime` a secas el cargador no encuentra `libFcitx5Utils.so.2` y la fase
+# muere con **`exit 127`** — que se lee como «no existe el ejecutable» y es «no existe una de sus
+# bibliotecas». Cuesta un rato porque el ejecutable SÍ está.
+#
+# La lista no se adivinó: se recorrió el `NEEDED` recursivamente desde los tres binarios contra los
+# artefactos del store. Clausura completa = `libime` + `fcitx5` + `zstd`, cero sonames huérfanos.
+# Es la misma figura que la cadena de tres saltos de `espeak-ng`, con una diferencia: allá el `.a`
+# del medio CORTABA la propagación; acá todo es `.so` y propaga, pero **declarar sigue siendo
+# manual** porque el sandbox es hermético a propósito.
+build = ["libime", "fcitx5", "zstd"]
diff --git a/recipes/libime-lm.toml b/recipes/libime-lm.toml
index 0e4254e0..e875aa32 100644
--- a/recipes/libime-lm.toml
+++ b/recipes/libime-lm.toml
@@ -83,4 +83,16 @@ echo "libime-lm: zh_CN.lm $(stat -c%s /out/usr/lib/libime/zh_CN.lm) B + zh_CN.lm
'''
[deps]
-build = ["libime"]
+# ⚠ **`fcitx5` y `zstd` NO son opcionales acá, y el fallo NO dice que falten.** Las herramientas de
+# libime son binarios DINÁMICOS: `libime_pinyindict` declara
+# `NEEDED libIMEPinyin.so.0 libIMECore.so.0 libFcitx5Utils.so.2`. El sandbox sólo monta lo
+# declarado, así que con `libime` a secas el cargador no encuentra `libFcitx5Utils.so.2` y la fase
+# muere con **`exit 127`** — que se lee como «no existe el ejecutable» y es «no existe una de sus
+# bibliotecas». Cuesta un rato porque el ejecutable SÍ está.
+#
+# La lista no se adivinó: se recorrió el `NEEDED` recursivamente desde los tres binarios contra los
+# artefactos del store. Clausura completa = `libime` + `fcitx5` + `zstd`, cero sonames huérfanos.
+# Es la misma figura que la cadena de tres saltos de `espeak-ng`, con una diferencia: allá el `.a`
+# del medio CORTABA la propagación; acá todo es `.so` y propaga, pero **declarar sigue siendo
+# manual** porque el sandbox es hermético a propósito.
+build = ["libime", "fcitx5", "zstd"]
diff --git a/recipes/libime-table.toml b/recipes/libime-table.toml
index 2c05e569..37d43809 100644
--- a/recipes/libime-table.toml
+++ b/recipes/libime-table.toml
@@ -75,4 +75,16 @@ echo "libime-table: $n tabla(s) compiladas en /usr/share/libime"
'''
[deps]
-build = ["libime"]
+# ⚠ **`fcitx5` y `zstd` NO son opcionales acá, y el fallo NO dice que falten.** Las herramientas de
+# libime son binarios DINÁMICOS: `libime_pinyindict` declara
+# `NEEDED libIMEPinyin.so.0 libIMECore.so.0 libFcitx5Utils.so.2`. El sandbox sólo monta lo
+# declarado, así que con `libime` a secas el cargador no encuentra `libFcitx5Utils.so.2` y la fase
+# muere con **`exit 127`** — que se lee como «no existe el ejecutable» y es «no existe una de sus
+# bibliotecas». Cuesta un rato porque el ejecutable SÍ está.
+#
+# La lista no se adivinó: se recorrió el `NEEDED` recursivamente desde los tres binarios contra los
+# artefactos del store. Clausura completa = `libime` + `fcitx5` + `zstd`, cero sonames huérfanos.
+# Es la misma figura que la cadena de tres saltos de `espeak-ng`, con una diferencia: allá el `.a`
+# del medio CORTABA la propagación; acá todo es `.so` y propaga, pero **declarar sigue siendo
+# manual** porque el sandbox es hermético a propósito.
+build = ["libime", "fcitx5", "zstd"]
diff --git a/recipes/libime.toml b/recipes/libime.toml
index 0afd3033..58a21079 100644
--- a/recipes/libime.toml
+++ b/recipes/libime.toml
@@ -38,8 +38,42 @@
# compila `libboost_iostreams.a` y está promovido al corpus (radio 1 verificado, `kactivitymanagerd`
# con el hash intacto).
#
-# `BUILD_SHARED_LIBS` se deja en On (su default): `fcitx5-chinese-addons` es un addon `.so` y
-# enlazarlo contra una libime estática no-PIC sería el muro del 19-oct otra vez.
+# ══ ⚠ `BUILD_SHARED_LIBS=Off` + PIC, Y NO ES PREFERENCIA: ES UN SEGFAULT MEDIDO ════════════════
+# Con las `.so` (el default de upstream) **`libime_pinyindict` revienta con CUALQUIER entrada,
+# incluso un fichero VACÍO**, mientras las otras tres herramientas funcionan. El diagnóstico salió
+# del kernel, no de una conjetura:
+#
+# segfault at 1064e20 ip 0000000001064e20 … error 15 in libime_pinyindict[…]
+#
+# `ip` == la dirección del fallo y es un **fetch de instrucción**: el flujo saltó a algo que no es
+# código. El binario es no-PIE, así que la dirección se resuelve directo en su tabla de símbolos, y
+# cae en `.data.rel.ro`, exactamente en:
+#
+# 0000000001064e20 d typeinfo for std::__1::collate<wchar_t>
+#
+# ⇒ **Hay CUATRO copias de libc++ en el proceso.** Medido: `libime_pinyindict`,
+# `libIMEPinyin.so.0`, `libIMECore.so.0` y `libFcitx5Utils.so.2` definen cada uno ~64 símbolos
+# `std::__1::locale::` propios, porque zig enlaza su libc++ **estáticamente** en cada módulo. El
+# despacho de facetas de `std::locale` cruza el límite de módulo y devuelve el typeinfo de una
+# faceta donde se espera un puntero a función. Es la familia de [[cxx-runtime-debe-coincidir]] y de
+# [[dos-glib-estaticas-en-un-proceso]], con el runtime de C++ en el papel de la biblioteca duplicada.
+#
+# Y explica el patrón exacto de lo que funciona y lo que no:
+# · `libime_slm_build_binary` kenlm entra ESTÁTICO en el exe ⇒ no cruza ⇒ funciona
+# · `libime_tabledict` usa `libIMETable.so`, pero su camino no toca facetas ⇒ funciona
+# · `libime_pinyindict` crea el `std::ifstream` en el EXE y lo consume dentro de
+# `libIMEPinyin.so` ⇒ cruza ⇒ segfault
+#
+# ⇒ `BUILD_SHARED_LIBS=Off` elimina el cruce: las bibliotecas entran en la herramienta y hay UNA
+# sola copia del runtime en el módulo que importa. **Y `CMAKE_POSITION_INDEPENDENT_CODE=ON` es
+# obligatorio con eso**: `fcitx5-chinese-addons` es un addon `.so` y una `.a` no-PIC no puede entrar
+# en una compartida — ése fue el muro del 19-oct.
+#
+# ⚠ **Lo que esto NO arregla, y conviene decirlo:** la duplicación sigue existiendo en todo el
+# corpus de C++ —cada `.so` lleva su libc++—, así que esta clase de fallo puede aparecer en
+# cualquier cadena donde un objeto con facetas de locale cruce módulos. Acá se evita el cruce, no se
+# cura la causa. La cura sería una libc++ COMPARTIDA en el corpus, y eso es una campaña propia: hay
+# que medirla antes de prometerla.
name = "libime"
version = "1.1.17"
license = "LGPL-2.1-or-later"
@@ -90,12 +124,33 @@ 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"
+# ⚠ **PARCHE: `kenlm` tiene que EXPORTARSE, y sólo hace falta porque construimos ESTÁTICO.**
+# `src/libime/core/CMakeLists.txt` declara `add_library(kenlm OBJECT …)`. Con `.so` sus objetos
+# entran dentro de `libIMECore.so` y no hay nada que exportar; con `.a` las deps —incluso privadas—
+# quedan en la interfaz del target, porque una estática no las resuelve, y cmake corta:
+#
+# CMake Error: install(EXPORT "LibIMECoreTargets" …) includes target "IMECore" which requires
+# target "kenlm" that is not in any export set.
+#
+# Upstream nunca construye estático, así que no es un bug suyo: es una combinación que no soporta.
+# El parche la hace soportable con lo mínimo — `kenlm` pasa de OBJECT a STATIC y entra al mismo
+# export set que `IMECore`. Los dos `sed` se COMPRUEBAN antes y después.
+PC=src/libime/core/CMakeLists.txt
+grep -q 'add_library(kenlm OBJECT' "$PC" || { echo "libime: $PC ya no declara 'add_library(kenlm OBJECT' — upstream cambió; REVISAR si el parche sigue haciendo falta" >&2; exit 1; }
+sed -i 's/add_library(kenlm OBJECT /add_library(kenlm STATIC /' "$PC"
+grep -q 'install(TARGETS IMECore EXPORT LibIMECoreTargets' "$PC" || { echo "libime: no encuentro el install(TARGETS IMECore EXPORT LibIMECoreTargets …) donde engancho kenlm" >&2; exit 1; }
+sed -i 's|^install(TARGETS IMECore EXPORT LibIMECoreTargets|install(TARGETS kenlm EXPORT LibIMECoreTargets ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT lib)\ninstall(TARGETS IMECore EXPORT LibIMECoreTargets|' "$PC"
+grep -q 'install(TARGETS kenlm EXPORT LibIMECoreTargets' "$PC" || { echo "libime: el parche de export de kenlm no se aplicó" >&2; exit 1; }
+grep -q 'add_library(kenlm STATIC' "$PC" || { echo "libime: kenlm no quedó como STATIC" >&2; exit 1; }
+echo "libime: kenlm parcheado a STATIC y exportado"
+
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 \
-DPKG_CONFIG_ARGN=--static \
-DBoost_USE_STATIC_LIBS=ON -DBoost_USE_STATIC_RUNTIME=ON \
+ -DBUILD_SHARED_LIBS=Off -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
-DENABLE_DATA=Off -DENABLE_TOOLS=On -DENABLE_TEST=Off -DENABLE_DOC=Off
'''
compile = "cmake --build build"
@@ -105,8 +160,15 @@ compile = "cmake --build build"
install = '''
set -e
DESTDIR=/out cmake --install build
-n=$(find /out/usr/lib -name 'libIME*.so*' 2>/dev/null | wc -l)
-[ "$n" -gt 0 ] || { echo "libime: no quedó ninguna libIME*.so — NO sello" >&2; ls -R /out/usr/lib 2>/dev/null | head -20 >&2; exit 1; }
+# Con `BUILD_SHARED_LIBS=Off` lo que queda son `.a`. El guardián se ajusta a lo que la receta
+# produce AHORA — pedir `.so` acá rechazaría el build correcto, que es el error que cometí cuatro
+# veces el 20-oct escribiendo controles contra mi modelo del artefacto y no contra el artefacto.
+n=$(find /out/usr/lib -name 'libIME*.a' 2>/dev/null | wc -l)
+[ "$n" -gt 0 ] || { echo "libime: no quedó ninguna libIME*.a — NO sello" >&2; ls -R /out/usr/lib 2>/dev/null | head -20 >&2; exit 1; }
+# Y que sean PIC de verdad, que es lo que permite que entren en el addon .so de chinese-addons.
+# Una `.a` no-PIC se sella igual y el muro aparece DOS recetas más abajo, al enlazar la compartida.
+a=$(find /out/usr/lib -name 'libIMEPinyin.a' | head -1)
+readelf -r "$a" 2>/dev/null | grep -q 'R_X86_64_\(GOTPCREL\|REX_GOTP\|PLT32\)' || { echo "libime: libIMEPinyin.a no parece PIC — no podría entrar en el addon .so de chinese-addons" >&2; exit 1; }
falta=""
for t in libime_slm_build_binary libime_prediction libime_pinyindict libime_tabledict; do
[ -x "/out/usr/bin/$t" ] || falta="$falta $t"