cosmic: la calculadora del lanzador ANDA — el stdin de qalc era el stdout

=123*456 → 56088 en el lanzador. libqalculate re-sellado b3:edb8d14d.

La causa del «no lee nada de la tubería» no era readline, ni el toolchain, ni la versión:
el binario salía DINÁMICO pese a link="static", porque libtool se come el -static que
inyecta el harness. Y en un musl dinámico stdin, stdout y stderr se resolvieron por copy
relocation a UNA SOLA ranura de BSS compartida —las tres en la misma dirección—, así que
fileno(stdin) daba 1: el stdin del programa era el stdout. Eso explica la asimetría que
parecía absurda, que qalc -f /dev/stdin funcionara (abre el fd por RUTA, sin tocar el
FILE* roto) y qalc leyendo stdin no.

Fix: make LDFLAGS="-all-static -no-pie", el mismo par que ya usa libxml2. Con él los tres
símbolos vuelven a tener direcciones distintas en .rodata. Regla para el runbook:
link="static" en la receta NO garantiza un binario estático — readelf -d y
nm -S | grep ' std' lo contestan en dos segundos.

Segundo muro: qalc pregunta si activar autocalc antes de leer nada, el plugin le manda la
expresión y cierra, la expresión se consume como respuesta y qalc nunca llega a guardar
config ⇒ vuelve a preguntar siempre. Se siembra qalc.cfg desde cosmic-start, política de
imagen como el color de fondo.

Y hay que repetir el -L de los alias de curses en el make: el LDFLAGS del make pisa al
del configure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-04 05:13:14 -04:00
co-authored by Claude Opus 5
parent 4d9f17dafb
commit 9665cc29d4
5 changed files with 95 additions and 48 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+57 -29
View File
@@ -108,7 +108,7 @@ Más `cosmic-settings-daemon` y `xdg-desktop-portal-cosmic` fuera de esa lista.
| **`cosmic-applets`** (19 binarios en uno) | ✅ **sellado** (necesitó `dbus-shared`, ver gotcha 10) | `b3:3e53a325` |
| `pop-launcher` 1.2.7 (otro repo, ver abajo) | ✅ sellado a la primera | `b3:64a5cbaa` |
| `gmp` 6.3.0 · `mpfr` 4.2.2 (cadena de la calculadora) | ✅ sellados a la primera | `b3:d99a0a5d` · `b3:13982d20` |
| `libqalculate` 5.12.0 (`qalc`) | ✅ sellado — **pero el plugin del lanzador no lo puede usar, ver abajo** | `b3:6bb435a7` |
| `libqalculate` 5.12.0 (`qalc`) | ✅ sellado — la calculadora del lanzador ANDA (ver abajo) | `b3:edb8d14d` |
**La cola está COMPLETA: 30 recetas, `escritorio-cosmic` 68/68.**
@@ -481,38 +481,66 @@ plugin, que contesta «qalc command is not installed»**. Esa respuesta es la me
disponible: el proceso hijo corrió, evaluó la consulta y devolvió una fila. Lo que falta ahora es
`libqalculate` (el binario `qalc`), que es un paquete, no un problema.
### 🧮 `libqalculate`: empaquetado y andando, e inservible para el plugin
### 🧮 `libqalculate`: la calculadora del lanzador, y un `stdin` que era `stdout`
`qalc` está sellado y **funciona**: `qalc -t '2+2'` → `4`, `-f fichero`, y el modo interactivo sobre
una terminal calcula perfecto (`123 × 456 = 56088`). La cadena que baja desde una tecla —`Super` →
cosmic-launcher → pop-launcher → plugin `calc` → `qalc` → **mpfr → gmp**— está entera en el catálogo.
**`=123*456` → `56088` en el lanzador** (`cosmic-calculadora-qemu-2026-08-04.png`). La cadena entera
desde una tecla está en el catálogo: `Super` → cosmic-launcher → pop-launcher → plugin `calc` →
`qalc` → **mpfr → gmp**.
**Y aun así el lanzador sigue mostrando una fila vacía.** El plugin no le pasa la cuenta por argv:
arranca `qalc` SIN expresión, con stdin y stdout en tubería, le ESCRIBE la expresión y cierra. Ese
modo, en nuestro binario, no lee nada. Acotado con dos invocaciones que sólo difieren en cuándo llega
el EOF:
Pero el camino hasta ahí tuvo dos muros que vale la pena dejar escritos, porque ninguno se parecía a
lo que era.
```sh
{ printf '123*456\n'; sleep 3; } | qalc # LEE (aparece la pregunta de primer arranque)
printf '123*456\n' | qalc # NO lee NADA: imprime el prompt y sale con rc=0
**1) `stdin` apuntaba al descriptor 1.** El plugin no le pasa la cuenta por argv: arranca `qalc` SIN
expresión, con stdin en tubería, le escribe la expresión y cierra. En ese modo el binario **no leía
nada** — ni error ni resultado, la fila del lanzador salía en blanco. El síntoma acusaba a readline,
al toolchain y a la versión, y no era ninguno:
```
DIAG-main isatty=0 feof=0 ferror=0 fd=1 ← fileno(stdin) == 1
nm -S qalc | grep -E ' (stdin|stdout|stderr)$'
1847148 8 B stderr
1847148 8 B stdin ← las TRES en la misma dirección
1847148 8 B stdout
```
O sea: **con el EOF ya pendiente en un stdin que no es terminal, `readline()` devuelve NULL antes de
entregar la línea que sí está en el búfer** — y así es exactamente como lo invoca el plugin.
La causa: **el binario salía DINÁMICO aunque la receta dice `link = "static"`, porque libtool se come
el `-static` que inyecta el harness** ([[link-static-mentira-libtool]] otra vez, cuarto caso). Y en
un musl dinámico las tres variables de stdio se resolvieron por *copy relocation* a **una sola ranura
de BSS compartida**: la última que escribe el loader gana, así que el `stdin` del programa era el
`stdout`. De ahí la asimetría que parecía absurda: `qalc -f /dev/stdin` funcionaba —abre el
descriptor por RUTA, sin tocar el `FILE*` roto— y `qalc` leyendo `stdin` no.
Lo que ya se DESCARTÓ midiendo, para que nadie lo repita:
- **No es el toolchain.** Una sonda estática musl del propio sandbox (C y C++) lee la tubería sin
problema. El binario no tiene nada raro: un solo `NEEDED`, `libc.so`.
- **No es readline vs no-readline.** Sin readline es peor (no lee nunca, ni con la tubería abierta);
con readline al menos lee cuando el escritor no ha cerrado.
- **No es la versión de readline.** Se construyó una 8.3 como sombra de cola y el síntoma no se
movió; la sombra se descartó en vez de dejar una receta duplicada que no arregla nada.
- **No es el descriptor 0.** `qalc -f /dev/stdin` —el MISMO pipe, reabierto por ruta— sí funciona.
El que no sirve es el `FILE*` `stdin`.
El fix es el par que ya usa `libxml2`: `make LDFLAGS="-all-static -no-pie"`. Con él el binario queda
estático de verdad y los tres símbolos vuelven a tener direcciones distintas, en `.rodata`:
La pista que queda es justamente esa última asimetría. Y el aviso de método: **empaquetar el binario
que falta no garantiza que el consumidor lo pueda usar** — «qalc no está instalado» y «qalc está y no
contesta» son dos estados distintos, y el segundo se ve peor porque la fila sale en blanco.
```
11e71c8 8 R stderr
11e71d0 8 R stdin
11e71d8 8 R stdout
```
**La regla, y es de las caras: `link = "static"` en la receta no garantiza un binario estático.**
Cuando algo se comporta como si su `stdin`/`stdout` estuvieran cruzados, `readelf -d` y
`nm -S | grep ' std'` contestan en dos segundos lo que el log no dice nunca.
**2) La pregunta de primer arranque se come la cuenta, SIEMPRE.** Antes de leer nada, `qalc` pregunta
si activar `autocalc`. El plugin le manda la expresión y cierra: la expresión se consume como
respuesta, no es un sí/no válido, y qalc **no llega a guardar config** ⇒ la próxima vez vuelve a
preguntar. No es «falla la primera vez»: no anda nunca. Se siembra
`~/.config/qalculate/qalc.cfg` con `calculate_as_you_type=0` desde `cosmic-start` — política de la
imagen, como el color de fondo; una distro de verdad lo pondría en `/etc/skel`.
**Gotchas de empaquetado de la cadena:**
- **`gmp` va con `--disable-assembly`.** Elige rutinas en ensamblador mirando la CPU de quien
compila, o sea hornearía la ISA del laptop en el artefacto y el hash dejaría de significar lo mismo
en la granja. Es una calculadora de escritorio: la implementación en C portable sobra.
- **El configure de libqalculate no encuentra readline** porque prueba `-lreadline` junto a
`-lncurses`, `-lcurses`, `-ltermcap` y `-ltinfo`, y el corpus **sólo empaqueta `libncursesw.a`**.
Que el paquete se llame `ncurses` no significa que provea `-lncurses`. Los alias van en un
directorio LOCAL (`.curses-compat/`) y no en `/usr/lib`: esa vista es la evidencia de qué se
declaró, y ensuciarla para que pase un test deja mintiendo justo al mecanismo que existe para no
mentir. Ojo: hay que repetir el `-L` en el `make`, porque el `LDFLAGS` del `make` PISA al del
`configure`.
### ⏱ El panel tiene un DEADLINE de arranque: con 4 vCPU sale mutilado
@@ -530,9 +558,9 @@ El número de procesos no distingue «arrancó» de «dibujó». El framebuffer
## Lo que falta, en orden
1. **Aplicaciones.** No hay ni una: los 20 `.desktop` de la imagen son `NoDisplay=true`. El primer
candidato barato es `libqalculate` (el binario `qalc`), que es lo único que le falta al plugin de
la calculadora para dar un resultado en vez de decir que no está.
1. **Aplicaciones con ventana.** Sigue sin haber ninguna: los 20 `.desktop` de la imagen son
`NoDisplay=true`, así que la biblioteca sale vacía y eso es correcto. La calculadora del lanzador
ya da resultados, pero es una fila de texto, no una app. `cosmic-term` sería la primera de verdad.
2. `xdg-desktop-portal-cosmic` — sin él no hay captura de pantalla, ni file-chooser, ni compartir.
3. `cosmic-settings` (el panel de control) y `cosmic-files`: la suite tiene aplicaciones y el
catálogo todavía no.
+14 -17
View File
@@ -3,9 +3,8 @@
"totals": {
"recipes": 800,
"nodes": 800,
"sealed": 797,
"never": 2,
"debt": 1
"sealed": 798,
"never": 2
},
"by_class": {
"c": 160,
@@ -15,7 +14,7 @@
"rust": 241
},
"debt_by_class": {
"c": 3
"c": 2
},
"by_queue": {
"corpus": {
@@ -23,8 +22,7 @@
"never": 2
},
"incoming-cosmic": {
"sealed": 29,
"debt": 1
"sealed": 30
}
},
"by_profile": {
@@ -41,8 +39,7 @@
"escritorio-cosmic": {
"raices": 20,
"clausura": 68,
"sealed": 67,
"debt": 1
"sealed": 68
},
"escritorio-mirada": {
"raices": 14,
@@ -4898,7 +4895,7 @@
"hash": "b3:d99a0a5d6fb0e301523c3797d899e7c1002c58f282865df87e221b4e47fd0642",
"state": "sealed",
"blocked_by": [],
"unblocks": 1,
"unblocks": 0,
"perfiles": [
"escritorio-cosmic"
],
@@ -8228,8 +8225,8 @@
],
"cls": "c",
"queue": "incoming-cosmic",
"hash": "b3:fcf9c6c097f9f37235319d52dbe64e42deaa012cc374aee206f9c5d2201ae3ac",
"state": "debt",
"hash": "b3:edb8d14db8e625168dea50e40073840b29a6800d7c5b027bbf01f17f15fd5b68",
"state": "sealed",
"blocked_by": [],
"unblocks": 0,
"perfiles": [
@@ -8452,7 +8449,7 @@
"hash": "b3:c4c65c4b17b86211c4a7f076e6693aedb9168d72cc8e45a4a9022fdd75f79494",
"state": "sealed",
"blocked_by": [],
"unblocks": 1,
"unblocks": 0,
"perfiles": [
"escritorio-cosmic",
"escritorio-gnome"
@@ -8804,7 +8801,7 @@
"hash": "b3:fbad44acdc76df4b081cccd3cc02666664b6d1a03cd6fa482f85791fa533a602",
"state": "sealed",
"blocked_by": [],
"unblocks": 2,
"unblocks": 1,
"perfiles": [
"base",
"cli",
@@ -9471,7 +9468,7 @@
"hash": "b3:13982d20e404affb9ab825c157a50efc2f0370e1befe75cb7ce93b914d55d497",
"state": "sealed",
"blocked_by": [],
"unblocks": 1,
"unblocks": 0,
"perfiles": [
"escritorio-cosmic"
],
@@ -9695,7 +9692,7 @@
"hash": "b3:a3be97622d20cd8d831a25403e00eccb23f2585ed78670a4e12d6cbe519fb8ec",
"state": "sealed",
"blocked_by": [],
"unblocks": 1,
"unblocks": 0,
"perfiles": [
"base",
"cli",
@@ -10617,7 +10614,7 @@
"hash": "b3:f0b5d0b1cb4321b3c61f24a871b9a58da3ebb7eb903af4015e35bd41a6465437",
"state": "sealed",
"blocked_by": [],
"unblocks": 1,
"unblocks": 0,
"perfiles": [
"base",
"cli",
@@ -11053,7 +11050,7 @@
"hash": "b3:03c0eb9ccaf42a65d876c0778b7ee53695f6389bbe5c4c9bcf606f8aa328ec36",
"state": "sealed",
"blocked_by": [],
"unblocks": 1,
"unblocks": 0,
"perfiles": [
"base",
"cli",
+11 -2
View File
@@ -69,8 +69,17 @@ for a in libncurses libcurses libtinfo libtermcap; do ln -sf /usr/lib/libncurses
--disable-nls \
LDFLAGS="$LDFLAGS -L$PWD/.curses-compat"
'''
compile = 'make -j"$(nproc)"'
install = 'make DESTDIR=/out install'
# `-all-static` NO ES REDUNDANTE con `link = "static"`, y esa confusión costó todo un frente.
# El harness inyecta `LDFLAGS=-static`, pero **libtool se lo come**: la línea de enlace real de `qalc`
# salía dinámica contra `libc.so`. Y un musl DINÁMICO acá no es sólo «pesa distinto»: `stdin`,
# `stdout` y `stderr` terminaron **en la misma dirección de BSS**, las tres compartiendo una única
# ranura de copy-relocation, así que el `stdin` del programa era en realidad el `stdout`
# (`fileno(stdin)` = 1). De ahí que `qalc` no leyera NADA de una tubería mientras `-f /dev/stdin`
# —que abre el descriptor por ruta, sin tocar el FILE* roto— funcionaba perfecto.
# `-all-static` es la forma de decírselo a libtool; `-no-pie` va con él (un estático PIE no tiene
# sentido). Mismo par que usa `libxml2` en el corpus, por la misma razón.
compile = 'make LDFLAGS="-all-static -no-pie -L$PWD/.curses-compat" -j"$(nproc)"'
install = 'make DESTDIR=/out install LDFLAGS="-all-static -no-pie -L$PWD/.curses-compat"'
[deps]
build = ["make", "gmp", "mpfr", "libxml2", "pkgconf", "readline", "ncurses"]
+13
View File
@@ -157,6 +157,19 @@ RON
say "fondo: color liso (el wallpaper por defecto vive en git-lfs y no se empaqueta)"
fi
# ── SEMILLA DE CONFIG DE qalc, SIN LA CUAL LA CALCULADORA DEL LANZADOR NUNCA ANDA ────────────────
# En el primer arranque `qalc` hace una PREGUNTA interactiva («¿activar autocalc?») antes de leer
# nada. El plugin `calc` de pop-launcher le escribe la expresión y cierra el pipe, así que la
# expresión se consume como respuesta, no es un sí/no válido, y qalc **no llega a guardar config** ⇒
# la próxima vez vuelve a preguntar. O sea que sin sembrar esto no falla una vez: falla SIEMPRE.
# Es política de la imagen, no del paquete —igual que el color de fondo—: una distro de verdad lo
# pondría en el esqueleto de /etc/skel.
if [ ! -f "$HOME/.config/qalculate/qalc.cfg" ]; then
mkdir -p "$HOME/.config/qalculate"
printf '[General]\ncalculate_as_you_type=0\n' > "$HOME/.config/qalculate/qalc.cfg"
say "qalc: config sembrada (si no, la pregunta de primer arranque se come la cuenta)"
fi
# ── MODO: bare (default) o session ──────────────────────────────────────────────────────────────
# `cosmic-session` es el camino de producción, pero HOY no se puede usar: llama a
# `cosmic-settings-daemon` con `.expect("failed to start settings daemon")` (src/main.rs:255) y