tawasuyu

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…

⎇ main recipes/libime.toml TOML · 186 siq'ikuna · 12.6 KB Ñawpa willay ch'uya

libime: estática y PIC, porque había CUATRO libc++ en un proceso bd9c765e1 · Sergio ·

1# libime 1.1.17 — el motor de conversión pinyin→hanzi. La pieza que faltaba para escribir chino.
2#
3# ══ QUÉ ES ═════════════════════════════════════════════════════════════════════════════════════
4# El chino se escribe tecleando la pronunciación en pinyin y eligiendo entre los caracteres que
5# suenan igual. Con decenas de miles de hanzi eso no es una tabla: es un modelo de lenguaje que
6# ordena los candidatos por probabilidad en contexto. libime es ese motor (un kenlm recortado más
7# los índices de pinyin y de tabla), y `fcitx5-chinese-addons` es su cara.
8#
9# ══ ⚠ `ENABLE_DATA=Off`, Y NO ES UN RECORTE: ES QUE LOS DATOS SE DESCARGAN ═════════════════════
10# `data/CMakeLists.txt` hace **dos descargas en tiempo de build** desde el servidor de fcitx:
11#     lm_sc.arpa-20260629.tar.zst   75 MB   → el modelo de lenguaje
12#     dict-20260907.tar.zst                 → el diccionario de pinyin
13# y el sandbox de takana no tiene red, a propósito: una fuente que se baja al construir no es
14# reproducible ni auditable. ⇒ los datos salen a **recetas hermanas** (`libime-lm`,
15# `libime-dict`) con su tarball pineado por sha256, que es el modelo del repo — y upstream hasta
16# nos da los sha en ese mismo CMakeLists, así que no hay que confiar en nadie para pinearlos.
17#
18# ⚠ **Por eso `ENABLE_TOOLS=On` es obligatorio acá y no adorno.** Las recetas de datos necesitan
19# los ejecutables de ESTA para convertir el `.arpa` y los `.txt` en los binarios que el motor lee.
20# Si esta receta se construyera sin herramientas, las otras no tendrían con qué trabajar.
21#
22# ⚠ **SON CUATRO, Y MI GUARDIÁN PEDÍA TRES — corregido 2026-10-20.** Había escrito el control contra
23# las herramientas que iban a usar `libime-lm` y `libime-dict` (`libime_slm_build_binary`,
24# `libime_prediction`, `libime_pinyindict`) sin leer el `data/CMakeLists.txt` completo de upstream,
25# que usa también **`LibIME::tabledict`** para los ocho diccionarios de los métodos por TABLA. Es la
26# misma clase de defecto que los cuatro guardianes demasiado estrictos del mismo día, con el signo
27# invertido: **escribí el guardián contra lo que yo pensaba usar, no contra lo que el paquete
28# promete.** Un guardián que pide menos de lo que hace falta no rechaza nada — deja pasar el hueco
29# hasta que una receta de datos dos pasos más abajo no encuentra su binario.
30#
31# ⇒ Y hasta que esas dos existan, **libime NO se declara en ningún perfil**: un motor de conversión
32# sin modelo de lenguaje carga y no convierte nada. Es la figura de `crun` sin podman, la misma por
33# la que `fcitx5` esperó un día a tener un motor.
34#
35# ══ LO QUE PIDE, MEDIDO EN SU CMakeLists ═══════════════════════════════════════════════════════
36#   ECM · PkgConfig · libzstd · Fcitx5Utils · **Boost 1.61 COMPONENTS iostreams**
37# El último obligó a cambiar `boost`, que en este corpus era headers-only desde hacía un año: ahora
38# compila `libboost_iostreams.a` y está promovido al corpus (radio 1 verificado, `kactivitymanagerd`
39# con el hash intacto).
40#
41# ══ ⚠ `BUILD_SHARED_LIBS=Off` + PIC, Y NO ES PREFERENCIA: ES UN SEGFAULT MEDIDO ════════════════
42# Con las `.so` (el default de upstream) **`libime_pinyindict` revienta con CUALQUIER entrada,
43# incluso un fichero VACÍO**, mientras las otras tres herramientas funcionan. El diagnóstico salió
44# del kernel, no de una conjetura:
45#
46#     segfault at 1064e20 ip 0000000001064e20 … error 15 in libime_pinyindict[…]
47#
48# `ip` == la dirección del fallo y es un **fetch de instrucción**: el flujo saltó a algo que no es
49# código. El binario es no-PIE, así que la dirección se resuelve directo en su tabla de símbolos, y
50# cae en `.data.rel.ro`, exactamente en:
51#
52#     0000000001064e20 d typeinfo for std::__1::collate<wchar_t>
53#
54# ⇒ **Hay CUATRO copias de libc++ en el proceso.** Medido: `libime_pinyindict`,
55# `libIMEPinyin.so.0`, `libIMECore.so.0` y `libFcitx5Utils.so.2` definen cada uno ~64 símbolos
56# `std::__1::locale::` propios, porque zig enlaza su libc++ **estáticamente** en cada módulo. El
57# despacho de facetas de `std::locale` cruza el límite de módulo y devuelve el typeinfo de una
58# faceta donde se espera un puntero a función. Es la familia de [[cxx-runtime-debe-coincidir]] y de
59# [[dos-glib-estaticas-en-un-proceso]], con el runtime de C++ en el papel de la biblioteca duplicada.
60#
61# Y explica el patrón exacto de lo que funciona y lo que no:
62#   · `libime_slm_build_binary`  kenlm entra ESTÁTICO en el exe ⇒ no cruza ⇒ funciona
63#   · `libime_tabledict`         usa `libIMETable.so`, pero su camino no toca facetas ⇒ funciona
64#   · `libime_pinyindict`        crea el `std::ifstream` en el EXE y lo consume dentro de
65#                                `libIMEPinyin.so` ⇒ cruza ⇒ segfault
66#
67# ⇒ `BUILD_SHARED_LIBS=Off` elimina el cruce: las bibliotecas entran en la herramienta y hay UNA
68# sola copia del runtime en el módulo que importa. **Y `CMAKE_POSITION_INDEPENDENT_CODE=ON` es
69# obligatorio con eso**: `fcitx5-chinese-addons` es un addon `.so` y una `.a` no-PIC no puede entrar
70# en una compartida — ése fue el muro del 19-oct.
71#
72# ⚠ **Lo que esto NO arregla, y conviene decirlo:** la duplicación sigue existiendo en todo el
73# corpus de C++ —cada `.so` lleva su libc++—, así que esta clase de fallo puede aparecer en
74# cualquier cadena donde un objeto con facetas de locale cruce módulos. Acá se evita el cruce, no se
75# cura la causa. La cura sería una libc++ COMPARTIDA en el corpus, y eso es una campaña propia: hay
76# que medirla antes de prometerla.
77name    = "libime"
78version = "1.1.17"
79license = "LGPL-2.1-or-later"
80
81# ⚠ **FUENTE GIT Y NO TARBALL, Y ES OBLIGATORIO: `kenlm` ES UN SUBMÓDULO.**
82# El modelo de lenguaje de libime no es código propio — es un kenlm recortado que entra por
83# submódulo (`.gitmodules`: `src/libime/core/kenlm` ← `https://github.com/kpu/kenlm.git`). Un
84# archive de GitHub **no materializa submódulos**, así que con el tarball el árbol llegaba con ese
85# directorio VACÍO y cmake cortaba así:
86#
87#     CMake Error at src/libime/core/CMakeLists.txt:12 (add_library):
88#       No SOURCES given to target: kenlm
89#     CMake Error at tools/CMakeLists.txt:1 (add_executable):
90#       No SOURCES given to target: libime_slm_build_binary
91#
92# O sea: la biblioteca Y las herramientas de datos, las dos por el mismo hueco.
93#
94# ⇒ `takana` **sí** materializa submódulos, pero sólo con fuente `repo`+`commit`: lee `.gitmodules`
95# y los SHA de gitlink y archiva cada uno en su subruta (`fetch::materializar_submodulos`). Con
96# `tarball` no hay nada que leer. Es la MISMA trampa que `yoga` en fcitx5, resuelta del otro lado:
97# allá el submódulo es de un tercero con su propio release, así que se pineó su tarball en una
98# receta aparte; acá kenlm se compila DENTRO de libime (`add_library(kenlm …)` es de libime), así
99# que no hay receta que sacar — hay que traer el árbol completo.
100#
101# ⚠ **Y yo tenía el dato y lo leí mal.** Midiendo el pico de RAM de la campaña corrí
102# `ls …/libime-1.1.17/src/libime/core/kenlm/lm/*.cc | wc -l`, salió **0**, y lo interpreté como
103# «me equivoqué de ruta» en vez de «el directorio está vacío porque es un gitlink». El cero era la
104# respuesta correcta a la pregunta que no me hice. Es el defecto que este repo tiene documentado
105# como *leí hasta donde me daba la razón*, y costó una vuelta entera de la cadena.
106#
107# El commit es el del tag `1.1.17` (`git/refs/tags/1.1.17` ⇒ `50de2779…`), no una rama: la versión
108# de arriba y esto nombran lo mismo.
109[source]
110repo   = "https://github.com/fcitx/libime"
111commit = "50de277992132ff59a6da64c88318f2a51a08f3d"
112
113[build]
114compiler = "zig-cc"
115target   = "x86_64-linux-musl"
116link     = "dynamic"
117flags    = []
118
119[build.phases]
120configure = '''
121ZW="$PWD/.zwrap"; mkdir -p "$ZW"
122# El envoltorio reescribe `--target`: es el patrón canónico del repo (30 recetas lo traen) y sin él
123# zig rechaza los triples que algunos builds inventan («UnknownOperatingSystem»).
124printf '%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"
125printf '%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"
126chmod +x "$ZW/cc" "$ZW/cxx"
127# ⚠ **PARCHE: `kenlm` tiene que EXPORTARSE, y sólo hace falta porque construimos ESTÁTICO.**
128# `src/libime/core/CMakeLists.txt` declara `add_library(kenlm OBJECT …)`. Con `.so` sus objetos
129# entran dentro de `libIMECore.so` y no hay nada que exportar; con `.a` las deps —incluso privadas—
130# quedan en la interfaz del target, porque una estática no las resuelve, y cmake corta:
131#
132#     CMake Error: install(EXPORT "LibIMECoreTargets" …) includes target "IMECore" which requires
133#     target "kenlm" that is not in any export set.
134#
135# Upstream nunca construye estático, así que no es un bug suyo: es una combinación que no soporta.
136# El parche la hace soportable con lo mínimo — `kenlm` pasa de OBJECT a STATIC y entra al mismo
137# export set que `IMECore`. Los dos `sed` se COMPRUEBAN antes y después.
138PC=src/libime/core/CMakeLists.txt
139grep -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; }
140sed -i 's/add_library(kenlm OBJECT /add_library(kenlm STATIC /' "$PC"
141grep -q 'install(TARGETS IMECore EXPORT LibIMECoreTargets' "$PC" || { echo "libime: no encuentro el install(TARGETS IMECore EXPORT LibIMECoreTargets …) donde engancho kenlm" >&2; exit 1; }
142sed -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"
143grep -q 'install(TARGETS kenlm EXPORT LibIMECoreTargets' "$PC" || { echo "libime: el parche de export de kenlm no se aplicó" >&2; exit 1; }
144grep -q 'add_library(kenlm STATIC' "$PC" || { echo "libime: kenlm no quedó como STATIC" >&2; exit 1; }
145echo "libime: kenlm parcheado a STATIC y exportado"
146
147rm -rf build
148cmake -B build -G Ninja -Wno-dev \
149  -DCMAKE_C_COMPILER="$ZW/cc" -DCMAKE_CXX_COMPILER="$ZW/cxx" \
150  -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr \
151  -DPKG_CONFIG_ARGN=--static \
152  -DBoost_USE_STATIC_LIBS=ON -DBoost_USE_STATIC_RUNTIME=ON \
153  -DBUILD_SHARED_LIBS=Off -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
154  -DENABLE_DATA=Off -DENABLE_TOOLS=On -DENABLE_TEST=Off -DENABLE_DOC=Off
155'''
156compile = "cmake --build build"
157# El guardián pide las DOS cosas por las que esta receta existe: las bibliotecas y las TRES
158# herramientas. Sin las herramientas, `libime-lm` y `libime-dict` no pueden construirse — y eso se
159# descubriría dos recetas más tarde, con el artefacto ya sellado y pareciendo correcto.
160install = '''
161set -e
162DESTDIR=/out cmake --install build
163# Con `BUILD_SHARED_LIBS=Off` lo que queda son `.a`. El guardián se ajusta a lo que la receta
164# produce AHORA — pedir `.so` acá rechazaría el build correcto, que es el error que cometí cuatro
165# veces el 20-oct escribiendo controles contra mi modelo del artefacto y no contra el artefacto.
166n=$(find /out/usr/lib -name 'libIME*.a' 2>/dev/null | wc -l)
167[ "$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; }
168# Y que sean PIC de verdad, que es lo que permite que entren en el addon .so de chinese-addons.
169# Una `.a` no-PIC se sella igual y el muro aparece DOS recetas más abajo, al enlazar la compartida.
170a=$(find /out/usr/lib -name 'libIMEPinyin.a' | head -1)
171readelf -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; }
172falta=""
173for t in libime_slm_build_binary libime_prediction libime_pinyindict libime_tabledict; do
174  [ -x "/out/usr/bin/$t" ] || falta="$falta $t"
175done
176if [ -n "$falta" ]; then
177  echo "libime: faltan herramientas:$falta — sin ellas las recetas de datos no pueden construirse" >&2
178  ls /out/usr/bin 2>/dev/null >&2
179  exit 1
180fi
181echo "libime: $n biblioteca(s) + las 4 herramientas de datos"
182'''
183
184[deps]
185build = ["cmake", "samurai", "extra-cmake-modules", "pkgconf", "fcitx5", "boost", "zstd",
186         "gettext-tiny", "glib", "pcre2", "libffi", "expat", "zlib", "dbus"]