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…
fix(espeak-ng): el cuarto guardián mío que rechaza un build correcto — y ya es un patrón
1# espeak-ng 1.52.0 — el sintetizador de voz. Con esto la distro HABLA.
2#
3# ══ POR QUÉ ENTRA: ES EL HUECO QUE DEJA GENTE AFUERA ═══════════════════════════════════════════
4# `censo-distro.py` marcaba «accesibilidad» **4 de 4 ausente**: sin lector de pantalla ni voz, una
5# distro es inusable para quien no ve — no incómoda, inusable. Y no hay forma de medir eso con las
6# otras métricas del repo: el tablero puede estar en 1158/1158 y clausura 10 de 10 mientras la
7# máquina es muda.
8#
9# espeak-ng es el eslabón de abajo y el que vale por sí solo: 100+ idiomas por síntesis formante,
10# sin modelos ni red, en unos pocos MB. Es lo que usan `orca` y `speech-dispatcher`, y también lo
11# que alguien invoca a mano (`espeak-ng -v es "hola"`).
12#
13# ══ ⚠ LOS DOS GUARDIANES SON EL PUNTO DE ESTA RECETA ═══════════════════════════════════════════
14# espeak-ng busca TODAS sus deps con `find_library`/`find_path` **sin REQUIRED**
15# (`cmake/deps.cmake`): sonic, pcaudio, mbrola. O sea que **construye y sella sin ninguna**, y
16# entonces:
17# · sin `pcaudio` → sintetiza a un `.wav` y **no reproduce**. `espeak-ng "hola"` no hace ruido.
18# · sin los datos de voz compilados → arranca y no tiene con qué pronunciar.
19# Las dos cosas son «sellado, con contenido, reproducible y sin la función que promete», que es la
20# figura que este catálogo persigue; en accesibilidad es peor, porque la promesa es que la máquina
21# habla. ⇒ el `install` comprueba que el binario SÍ enlace pcaudio y que los datos estén.
22#
23# ══ LAS PERILLAS ═══════════════════════════════════════════════════════════════════════════════
24# BUILD_SHARED_LIBS=ON `speech-dispatcher` y `orca` cargan `libespeak-ng.so` con dlopen; el
25# default de upstream es OFF, que dejaría sólo la `.a`
26# USE_MBROLA=OFF voces MBROLA: binario externo, no está en el corpus y son diccionarios
27# por idioma aparte. La síntesis formante propia no las necesita
28# USE_SPEECHPLAYER=OFF sintetizador alternativo experimental
29#
30# ⚠ **`sonic` ES DEP OBLIGATORIA AUNQUE SU `find_library` NO LO PAREZCA.** `deps.cmake` la busca sin
31# REQUIRED… y si no la encuentra **la CLONA de git** (`FetchContent_Declare(sonic-git …)`), lo que en
32# un sandbox sin red mata el build:
33# CMake Error at sonic-git-subbuild/…/sonic-git-populate-gitclone.cmake:50
34# Y `USE_LIBSONIC=OFF` no lo evita: el fetch está antes de que la opción se evalúe, y la rama
35# `else()` pone `HAVE_LIBSONIC ON` igual. ⇒ `recipes/sonic.toml` existe para que el `find_library`
36# acierte, pineada al MISMO commit que espeak-ng declara. Lección: **un `find_library` sin REQUIRED
37# no significa «opcional»; hay que leer el `else()`.**
38# ESPEAK_COMPAT=ON instala también el nombre viejo `espeak`, que es el que invocan los
39# scripts y las apps que no se actualizaron a `espeak-ng`
40#
41# ⚠ **LOS `-l` DE PULSE Y ALSA VAN A MANO, y es el problema transitivo del corpus estático otra
42# vez.** Con `sonic` resuelto, el build llegó a enlazar y murió así:
43# Error relocating /src/build/src/libespeak-ng/libespeak-ng.so.1: pa_strerror: symbol not found
44# Error relocating …: snd_strerror: symbol not found
45# `libpcaudio.a` referencia esos símbolos, pero espeak-ng la encuentra con
46# `find_library(PCAUDIO_LIB pcaudio)` — que devuelve **la ruta del `.a` y nada más**, sin las
47# transitivas. Y pcaudiolib no publica `.pc` (upstream no trae plantilla), así que no hay de dónde
48# sacarlas automáticamente. Medido con `nm`: `pa_strerror` está en `libpulse.so` y `snd_strerror` en
49# `libasound.so`.
50#
51# ⇒ Es la MISMA figura que `fontconfig.pc → Requires.private: expat` en fcitx5, con una diferencia
52# que importa: allá la cura fue `PKG_CONFIG_ARGN=--static` porque el `.pc` existía y sabía sus
53# privadas; **acá no hay `.pc`, así que las transitivas se escriben a mano o no hay forma.** Y el
54# fallo sale al EJECUTAR, no al enlazar —la fase de datos corre el propio `espeak-ng` para compilar
55# los fonemas—, que es por qué el síntoma es «symbol not found» y no «undefined reference».
56name = "espeak-ng"
57version = "1.52.0"
58license = "GPL-3.0-or-later"
59
60[source]
61tarball = "https://github.com/espeak-ng/espeak-ng/archive/refs/tags/1.52.0/espeak-ng-1.52.0.tar.gz"
62sha256 = "bb4338102ff3b49a81423da8a1a158b420124b055b60fa76cfb4b18677130a23"
63
64[build]
65compiler = "zig-cc"
66target = "x86_64-linux-musl"
67link = "dynamic"
68flags = []
69
70[build.phases]
71configure = '''
72ZW="$PWD/.zwrap"; mkdir -p "$ZW"
73# El envoltorio reescribe `--target`: patrón canónico del repo (30 recetas) para builds que inventan
74# su propio triple y hacen que zig corte con «UnknownOperatingSystem».
75printf '%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"
76printf '%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"
77chmod +x "$ZW/cc" "$ZW/cxx"
78rm -rf build
79cmake -B build -G Ninja -Wno-dev \
80 -DCMAKE_C_COMPILER="$ZW/cc" -DCMAKE_CXX_COMPILER="$ZW/cxx" \
81 -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr \
82 -DBUILD_SHARED_LIBS=ON -DUSE_MBROLA=OFF -DUSE_SPEECHPLAYER=OFF \
83 -DESPEAK_COMPAT=ON -DBUILD_TESTING=OFF \
84 -DCMAKE_SHARED_LINKER_FLAGS="-lpulse-simple -lpulse -lasound" \
85 -DCMAKE_EXE_LINKER_FLAGS="-lpulse-simple -lpulse -lasound"
86'''
87compile = "cmake --build build"
88install = '''
89set -e
90DESTDIR=/out cmake --install build
91[ -x /out/usr/bin/espeak-ng ] || { echo "espeak-ng: no quedó el binario — NO sello" >&2; exit 1; }
92so=$(find /out/usr/lib -name 'libespeak-ng.so*' -type f 2>/dev/null | head -1)
93[ -n "$so" ] || { echo "espeak-ng: no quedó libespeak-ng.so — orca y speech-dispatcher la cargan con dlopen" >&2; exit 1; }
94
95# ⚠ GUARDIÁN 1: que REPRODUZCA, no sólo que sintetice.
96#
97# **ESTE CONTROL ESTUVO MAL Y RECHAZÓ UN BUILD CORRECTO.** Mi primera versión miraba
98# `readelf -d` del BINARIO buscando «pcaudio», y eso no puede funcionar por dos razones que se
99# podían comprobar en un minuto:
100# · `libpcaudio` es un `.a`: un estático se enlaza DENTRO y **nunca** produce una entrada NEEDED.
101# · el binario sólo enlaza `libespeak-ng.so.1`; el audio vive en la `.so`, no en él.
102# Y su respaldo por `strings` buscaba `pulse_simple`, que no es el nombre de ningún símbolo
103# (`pa_simple_new` sí). O sea: tres comprobaciones y las tres contra un modelo mío del artefacto en
104# vez de contra el artefacto.
105#
106# ⇒ Lo de abajo son HECHOS medidos sobre un build correcto (cmake dijo
107# `libpcaudio: ON (/usr/lib/libpcaudio.a)` y `USE_LIBPCAUDIO:BOOL=ON`):
108# la `.so` trae `libpulse-simple.so.0 libpulse.so.0 libasound.so.2` en NEEDED, y exporta los
109# símbolos `audio_object_*` que son la API de pcaudiolib.
110# Se piden las dos cosas porque cubren fallos distintos: los NEEDED prueban que las bibliotecas de
111# audio están enlazadas; los símbolos, que el código de pcaudio entró de verdad.
112sim=$(nm -D "$so" 2>/dev/null | grep -c ' T audio_object_' || true)
113[ "${sim:-0}" -gt 0 ] || { echo "espeak-ng: la .so no exporta ningún audio_object_* — el código de pcaudiolib NO entró, sintetizaría a fichero y no sonaría" >&2; exit 1; }
114need=$(readelf -d "$so" 2>/dev/null | grep NEEDED | grep -cE 'libpulse|libasound' || true)
115[ "${need:-0}" -gt 0 ] || { echo "espeak-ng: la .so no enlaza ni libpulse ni libasound — no tiene a dónde sacar el sonido" >&2; readelf -d "$so" 2>/dev/null | grep NEEDED >&2; exit 1; }
116
117# ⚠ GUARDIÁN 2: los datos de voz. Se compilan desde la fuente (`cmake/data.cmake`), no se bajan —
118# pero si la fase de datos se saltara, quedaría un sintetizador sin fonemas que no pronuncia nada.
119d=$(find /out/usr/share/espeak-ng-data -type f 2>/dev/null | wc -l)
120[ "$d" -gt 50 ] || { echo "espeak-ng: sólo $d fichero(s) en espeak-ng-data — esperaba >50; sin fonemas ni voces no pronuncia" >&2; exit 1; }
121echo "espeak-ng: $sim símbolo(s) de audio + $need lib(s) de sonido en NEEDED + $d fichero(s) de datos"
122'''
123[deps]
124# ⚠ `binutils` está acá por los GUARDIANES, no por el build: aportan `readelf` y `strings`, y sin
125# ellos el control de audio fallaría por falta de herramienta en vez de por el defecto que busca —
126# un guardián que se cae solo acusa a la receta de algo que no hizo.
127# ⚠ **`libsndfile` NO es dep de espeak-ng: es dep de la cadena que pcaudiolib arrastra.** Con
128# `-lpulse` resuelto, el cargador cayó un nivel más abajo:
129# Error relocating /usr/lib/pulseaudio/libpulsecommon-17.0.so: sf_command: symbol not found
130# `libpulse` carga `libpulsecommon`, que declara `NEEDED libsndfile.so.1` (medido con readelf). Como
131# el sandbox sólo monta lo declarado, había que declararla acá aunque espeak-ng no la nombre nunca.
132# ⇒ La cadena completa, que es lo que hay que tener en la cabeza para este corpus:
133# espeak-ng → libpcaudio.a → libpulse.so → libpulsecommon.so → libsndfile.so.1
134# Tres saltos, y el `.a` del medio corta la propagación automática porque un `.a` no declara
135# NEEDED. Se declaran también las deps de runtime de pulseaudio (`glib`, `libffi`, `pcre2`) por el
136# mismo motivo.
137build = ["cmake", "samurai", "pkgconf", "pcaudiolib", "sonic", "pulseaudio", "alsa-lib",
138 "libsndfile", "glib", "libffi", "pcre2", "binutils"]