# libime 1.1.17 — el motor de conversión pinyin→hanzi. La pieza que faltaba para escribir chino. # # ══ QUÉ ES ═════════════════════════════════════════════════════════════════════════════════════ # El chino se escribe tecleando la pronunciación en pinyin y eligiendo entre los caracteres que # suenan igual. Con decenas de miles de hanzi eso no es una tabla: es un modelo de lenguaje que # ordena los candidatos por probabilidad en contexto. libime es ese motor (un kenlm recortado más # los índices de pinyin y de tabla), y `fcitx5-chinese-addons` es su cara. # # ══ ⚠ `ENABLE_DATA=Off`, Y NO ES UN RECORTE: ES QUE LOS DATOS SE DESCARGAN ═════════════════════ # `data/CMakeLists.txt` hace **dos descargas en tiempo de build** desde el servidor de fcitx: # lm_sc.arpa-20260629.tar.zst 75 MB → el modelo de lenguaje # dict-20260907.tar.zst → el diccionario de pinyin # y el sandbox de takana no tiene red, a propósito: una fuente que se baja al construir no es # reproducible ni auditable. ⇒ los datos salen a **recetas hermanas** (`libime-lm`, # `libime-dict`) con su tarball pineado por sha256, que es el modelo del repo — y upstream hasta # nos da los sha en ese mismo CMakeLists, así que no hay que confiar en nadie para pinearlos. # # ⚠ **Por eso `ENABLE_TOOLS=On` es obligatorio acá y no adorno.** Las recetas de datos necesitan # los ejecutables de ESTA para convertir el `.arpa` y los `.txt` en los binarios que el motor lee. # Si esta receta se construyera sin herramientas, las otras no tendrían con qué trabajar. # # ⚠ **SON CUATRO, Y MI GUARDIÁN PEDÍA TRES — corregido 2026-10-20.** Había escrito el control contra # las herramientas que iban a usar `libime-lm` y `libime-dict` (`libime_slm_build_binary`, # `libime_prediction`, `libime_pinyindict`) sin leer el `data/CMakeLists.txt` completo de upstream, # que usa también **`LibIME::tabledict`** para los ocho diccionarios de los métodos por TABLA. Es la # misma clase de defecto que los cuatro guardianes demasiado estrictos del mismo día, con el signo # invertido: **escribí el guardián contra lo que yo pensaba usar, no contra lo que el paquete # promete.** Un guardián que pide menos de lo que hace falta no rechaza nada — deja pasar el hueco # hasta que una receta de datos dos pasos más abajo no encuentra su binario. # # ⇒ Y hasta que esas dos existan, **libime NO se declara en ningún perfil**: un motor de conversión # sin modelo de lenguaje carga y no convierte nada. Es la figura de `crun` sin podman, la misma por # la que `fcitx5` esperó un día a tener un motor. # # ══ LO QUE PIDE, MEDIDO EN SU CMakeLists ═══════════════════════════════════════════════════════ # ECM · PkgConfig · libzstd · Fcitx5Utils · **Boost 1.61 COMPONENTS iostreams** # El último obligó a cambiar `boost`, que en este corpus era headers-only desde hacía un año: ahora # compila `libboost_iostreams.a` y está promovido al corpus (radio 1 verificado, `kactivitymanagerd` # con el hash intacto). # # ══ ⚠ `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 # # ⇒ **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" # ⚠ **FUENTE GIT Y NO TARBALL, Y ES OBLIGATORIO: `kenlm` ES UN SUBMÓDULO.** # El modelo de lenguaje de libime no es código propio — es un kenlm recortado que entra por # submódulo (`.gitmodules`: `src/libime/core/kenlm` ← `https://github.com/kpu/kenlm.git`). Un # archive de GitHub **no materializa submódulos**, así que con el tarball el árbol llegaba con ese # directorio VACÍO y cmake cortaba así: # # CMake Error at src/libime/core/CMakeLists.txt:12 (add_library): # No SOURCES given to target: kenlm # CMake Error at tools/CMakeLists.txt:1 (add_executable): # No SOURCES given to target: libime_slm_build_binary # # O sea: la biblioteca Y las herramientas de datos, las dos por el mismo hueco. # # ⇒ `takana` **sí** materializa submódulos, pero sólo con fuente `repo`+`commit`: lee `.gitmodules` # y los SHA de gitlink y archiva cada uno en su subruta (`fetch::materializar_submodulos`). Con # `tarball` no hay nada que leer. Es la MISMA trampa que `yoga` en fcitx5, resuelta del otro lado: # allá el submódulo es de un tercero con su propio release, así que se pineó su tarball en una # receta aparte; acá kenlm se compila DENTRO de libime (`add_library(kenlm …)` es de libime), así # que no hay receta que sacar — hay que traer el árbol completo. # # ⚠ **Y yo tenía el dato y lo leí mal.** Midiendo el pico de RAM de la campaña corrí # `ls …/libime-1.1.17/src/libime/core/kenlm/lm/*.cc | wc -l`, salió **0**, y lo interpreté como # «me equivoqué de ruta» en vez de «el directorio está vacío porque es un gitlink». El cero era la # respuesta correcta a la pregunta que no me hice. Es el defecto que este repo tiene documentado # como *leí hasta donde me daba la razón*, y costó una vuelta entera de la cadena. # # El commit es el del tag `1.1.17` (`git/refs/tags/1.1.17` ⇒ `50de2779…`), no una rama: la versión # de arriba y esto nombran lo mismo. [source] repo = "https://github.com/fcitx/libime" commit = "50de277992132ff59a6da64c88318f2a51a08f3d" [build] compiler = "zig-cc" target = "x86_64-linux-musl" link = "dynamic" flags = [] [build.phases] configure = ''' ZW="$PWD/.zwrap"; mkdir -p "$ZW" # El envoltorio reescribe `--target`: es el patrón canónico del repo (30 recetas lo traen) y sin él # zig rechaza los triples que algunos builds inventan («UnknownOperatingSystem»). 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" # El guardián pide las DOS cosas por las que esta receta existe: las bibliotecas y las TRES # herramientas. Sin las herramientas, `libime-lm` y `libime-dict` no pueden construirse — y eso se # descubriría dos recetas más tarde, con el artefacto ya sellado y pareciendo correcto. install = ''' set -e DESTDIR=/out cmake --install build # 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" done if [ -n "$falta" ]; then echo "libime: faltan herramientas:$falta — sin ellas las recetas de datos no pueden construirse" >&2 ls /out/usr/bin 2>/dev/null >&2 exit 1 fi echo "libime: $n biblioteca(s) + las 4 herramientas de datos" ''' [deps] build = ["cmake", "samurai", "extra-cmake-modules", "pkgconf", "fcitx5", "boost", "zstd", "gettext-tiny", "glib", "pcre2", "libffi", "expat", "zlib", "dbus"]