Files
Sergio 3b36e9df0a zsh: dejar de pedir install.info — fallaba siempre y sellaba un /usr/share/info VACÍO
`makeinfo` no está en el lab, así que `make … install install.info` moría con
`/bin/sh: makeinfo: not found` ⇒ `Error 2` en TODOS los builds. Y el artefacto sellaba igual: las
fases corren con `/bin/sh -c` sin `-e` (`takana-build/src/sandbox.rs:332,491`), o sea que un fallo
a mitad de fase no aborta — sólo cuenta el estado del ÚLTIMO comando, que acá era el bucle de
guardianes y daba 0.

Lo que llegaba al store era un zsh por lo demás correcto (estático, con sus módulos, con `=~`) pero
con un `/usr/share/info` vacío adentro. Es la regla 3 de CLAUDE.md a escala de fase en vez de
artefacto: un ausente falla ruidosamente, un vacío llega hasta el final diciendo que todo fue bien.

Lo que salvó al artefacto no fue el estado de salida sino los dos guardianes que la receta ya traía
escritos a mano. Una receta sin guardianes propios no tiene esa red — eso es el hallazgo general, y
va aparte.
2026-09-18 16:27:01 +00:00

184 lines
10 KiB
TOML

# zsh 5.9 — **el shell del usuario**, y por eso entra al piso y no a los extras.
#
# ══ POR QUÉ ESTA RECETA EXISTE ═════════════════════════════════════════════════════════════════
# Barrido del 2026-09-16 sobre la laptop del usuario: su shell de login es zsh (1611 `cd`, 1054
# `cargo`, 134 `zellij` en el historial), y zsh **no tenía receta en el corpus ni en ninguna de las
# cuatro colas**. Una imagen de takana instalada en esa máquina la dejaba en `bash` — funciona, pero
# no es su máquina. Es el mismo hueco de RUNTIME que `bash-completion` en `[perfil.base]`: nadie lo
# alcanza por deps de build, así que ninguna métrica de deuda podía verlo.
#
# ⚠ **PUNTO DE PARTIDA, NO SELLADA.** Escrita en el laptop, que no tiene lab ni store (su
# `.dev-fs/alpine` tiene 104 paquetes contra los 110 de `lab-toolchain.lock`, así que ni siquiera
# calcula hashes comparables). El primer build va en el worker. Lo que sigue son las trampas
# conocidas de hermanos ya sellados, anotadas para que el primer build no las redescubra.
#
# ══ LOS SEIS PARCHES SON DE ALPINE, Y NO SON OPCIONALES ════════════════════════════════════════
# Vienen de `main/zsh` de aports y son el trabajo de portabilidad a musl que un import de nix
# pierde. `implicit.patch` (16 K) es el gordo: declaraciones implícitas que gcc14/clang rechazan
# como error. Los otros cinco son tests que no aplican fuera de glibc, el instalador de usuario
# nuevo de Alpine, punteros incompatibles de gcc14 y el cacheo de `man -w`.
#
# ══ RIESGO CONOCIDO PARA EL PRIMER BUILD (heredado de bash.toml) ═══════════════════════════════
# ✅ **PASÓ, Y LA PREDICCIÓN ERA CORRECTA** (primer build, 2026-09-17): el artefacto salió
# `dynamically linked, interpreter /lib/ld-musl-x86_64.so.1` y murió con el síntoma exacto anotado
# acá arriba:
#
# Error relocating /lib/libncursesw.so.6: __vfprintf_chk: symbol not found
#
# —o sea, se enlazó contra la ncurses del LAB de Alpine, que ninguna imagen de takana tiene—.
#
# La causa de fondo es más honda que el `-rdynamic` de bash: **zsh construye sus módulos como `.so`
# y los carga con `dlopen`**, y un binario que hace `dlopen` no puede ser estático. Por eso el
# arreglo no es sólo tirar la bandera, es `--disable-dynamic`: los módulos quedan ENLAZADOS DENTRO
# del binario. Se pierde `zmodload` de un `.so` de fuera —que en una distro de artefactos sellados
# no tiene sentido— y se gana un zsh que arranca. `make LOCAL_LDFLAGS=` queda igual, por si el
# `configure` mete `-rdynamic` de todas formas.
#
# Y el control **va en la receta**, no en la cabeza de quien la corra: la fase de instalación
# pregunta por el `INTERP` del ELF con el `readelf` de binutils (que ya es dep) y CORTA si aparece.
# Un zsh dinámico sella, reproduce y no arranca: es [[needed-colgante-libstdcxx]] otra vez, y sin el
# guardián no se descubre hasta que alguien abre una terminal dentro de la imagen.
#
# ══ Y `--disable-dynamic` NO LINKEA LOS MÓDULOS: LOS APAGA ═════════════════════════════════════
# Segundo build, y el binario ya era estático — pero `zmodload -L` decía **`zsh/main` y nada más**:
#
# $ zsh -c '[[ "takana-2026" =~ "^takana-[0-9]+$" ]]'
# zsh:1: failed to load module: zsh/regex
# zsh:1: -regex-match not available for regex
#
# O sea un zsh sin `=~`, que es de las cosas que más se escriben en un script. Los que sí estaban
# —`zsh/complete`, `zsh/zle`— confunden más, porque el completado funciona y parece que está todo.
#
# ⚠ **Dos explicaciones mías fueron FALSAS antes de la buena. Las dejo escritas porque las dos son
# plausibles y cuestan un build cada una.**
#
# 1ª) «`--disable-dynamic` deja los módulos en `link=dynamic` y hay que pasarlos a `static` con un
# `sed` sobre `config.modules`». El `sed` no cambió nada: los disponibles YA salían `static`.
# 2ª) «el `link=` del `.mdd` evalúa `$ac_cv_func_regcomp` y `config.status` no tiene esas variables,
# así que hay que EXPORTARLAS». Tampoco: el configure contesta `yes` a las cuatro y el módulo
# seguía fuera.
#
# Lo que zanjó el asunto fue leer `configure.ac`, que no opina — decide:
#
# case "$link" in
# dynamic) if test x$dynamic != xno; then … else link=no fi ;; ← ACÁ
# either) if test x$dynamic != xno; then … else link=static fi ;;
#
# O sea: con `--disable-dynamic`, un módulo que su `.mdd` declara **`dynamic` se DESCARTA**, y sólo
# los que declaran **`either`** pasan a estático. `Src/Modules/regex.mdd` dice `echo dynamic`, y
# `Src/Zle/complete.mdd` dice `either`: por eso había completado y no había `=~`. No era el entorno
# ni el orden: era el vocabulario del `.mdd`.
#
# Por eso el arreglo va sobre los `.mdd`, ANTES del configure —`dynamic` → `either`—, que además
# sobrevive a que `make` regenere `config.modules` (lo regenera: corre `config.status` de nuevo).
# A los que les falta su librería el `.mdd` les hace decir `no`, y esos siguen fuera, que es correcto.
#
# La lección general: `checking for regcomp... yes` en el log **no** implica que el módulo que lo
# necesita se construya. Lo que manda es `config.modules`, y ahí el estado era `link=no`.
#
# ══ `--enable-pcre` ERA UNA ETIQUETA, NO UN HECHO ══════════════════════════════════════════════
# zsh 5.9 sólo habla **PCRE1** (busca `pcre-config` y `pcre.h`); el corpus tiene **pcre2**. El
# configure lo dijo tres veces —`checking for pcre-config... no`, `pcre.h... no`,
# `pcre_compile... no`— y la receta seguía declarando la bandera y la dep. Se quitan las dos: una dep
# que no se usa miente sobre el cierre y una bandera que no engancha miente sobre la capacidad.
# `=~` no la necesita: lo resuelve `zsh/regex` sobre la regex de libc.
name = "zsh"
version = "5.9"
license = "MIT"
[source]
tarball = "https://sourceforge.net/projects/zsh/files/zsh/5.9/zsh-5.9.tar.xz"
# sha256 calculado sobre el tarball bajado el 2026-09-16 (Alpine publica sha512, no sha256).
sha256 = "9b8d1ecedd5b5e81fbf1918e876752a7dd948e05c1a0dba10ab863842d45acd5"
patches = [
"implicit.patch",
"fix-gcc14-incompatible-pointer-types.patch",
"zsh-newuser-install-alpine.patch",
"0001-50278-use-man-w-in-preference-to-manpath-fix-caching.patch",
"skip-test-failing-on-musl.patch",
"tests-user.patch",
]
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "static"
# Split de la info de depuración (SDD 23 etapa 4). Entra en `hash_inputs`.
strip_debug = true
flags = []
[build.phases]
# de build() de Alpine (traducido; el lab provee $CBUILD/$CHOST).
#
# `--enable-function-subdirs` + `--enable-fndir`: zsh instala ~1200 ficheros de funciones en
# `/usr/share/zsh/functions`. NO se recortan: sin ellas no hay `compinit`, y un zsh sin completado
# es justamente lo que el usuario no tiene hoy. Es la lección de CLAUDE.md §3 al revés — acá el
# artefacto "completo" son los datos, no el binario.
#
# `--disable-gdbm` es deliberado: gdbm no está en el corpus y sólo aporta el módulo `zsh/db/gdbm`.
# Encenderlo pediría una receta más para una capacidad que nadie pidió.
compile = '''
_abuild_phase() {
# Ver la nota de arriba: con `--disable-dynamic`, lo que el `.mdd` declara `dynamic` se DESCARTA
# y sólo lo que declara `either` queda estático. Esto reescribe ese vocabulario antes de que el
# configure lo lea; los `.mdd` que dicen `no` por falta de librería siguen diciendo `no`.
sed -i "s/echo dynamic/echo either/; s/^link=dynamic$/link=either/" Src/*/*.mdd Src/*/*/*.mdd
./configure \
--build=$CBUILD \
--host=$CHOST \
--prefix=/usr \
--bindir=/bin \
--enable-etcdir=/etc/zsh \
--enable-cap \
--enable-multibyte \
--enable-function-subdirs \
--enable-fndir=/usr/share/zsh/functions \
--enable-zsh-secure-free \
--disable-gdbm \
--disable-dynamic \
--sysconfdir=/etc \
--mandir=/usr/share/man \
--infodir=/usr/share/info \
--with-tcsetpgrp
# Los módulos ADENTRO del binario: `--disable-dynamic` por sí solo los deja fuera.
make LOCAL_LDFLAGS=
}
_abuild_phase
'''
# de package() de Alpine (traducido $pkgdir→/out). Sin locales: el corpus no los hidrata y pesan.
install = '''
_abuild_phase() {
make LOCAL_LDFLAGS= DESTDIR="/out" install
rm -rf "/out"/usr/share/locale
# `install.info` NO se pide, y no es una poda de tamaño como la de arriba: MEDIDO el
# 2026-09-18, `makeinfo` no está en el lab, así que fallaba SIEMPRE
# (`/bin/sh: makeinfo: not found` ⇒ `make: *** [Makefile:255: install.info] Error 2`).
# Y el build SELLABA IGUAL, dejando un `/usr/share/info` vacío adentro: las fases corren
# con `/bin/sh -c` SIN `-e` (`takana-build/src/sandbox.rs:332,491`), así que un fallo a
# mitad de fase no aborta nada — sólo cuenta el estado del ÚLTIMO comando. Un directorio
# vacío en el artefacto es la regla 3 de CLAUDE.md en pequeño: llega hasta el final
# diciendo que todo fue bien. Pedir algo que no puede funcionar y tolerar su fallo es
# peor que no pedirlo.
rm -rf "/out"/usr/share/info
# `/etc/shells` no se escribe acá: es estado del sistema instalado, no del artefacto.
# GUARDIÁN: un zsh dinámico sella, reproduce y NO ARRANCA dentro de una imagen (pide
# libncursesw.so.6, que nadie provee). Se comprueba el hecho en el ELF, no la intención.
if readelf -l /out/bin/zsh | grep -q INTERP; then
echo "zsh: el binario salió DINÁMICO (tiene INTERP) — pediría libncursesw.so.6 dentro de la imagen y moriría con 'Error relocating'. Mirar si el configure volvió a meter -rdynamic"
exit 1
fi
# Y que los módulos estén DENTRO: un zsh sin `zsh/regex` no tiene `=~`, y eso no se nota
# hasta que un script lo usa. Se pregunta al binario, que es quien sabe.
for m in zsh/regex zsh/complete zsh/zle zsh/parameter; do
if ! /out/bin/zsh -fc "zmodload $m" 2>/dev/null; then
echo "zsh: falta el módulo $m dentro del binario — mirar config.modules tras el configure (¿quedó link=dynamic?)"
exit 1
fi
done
}
_abuild_phase
'''
[deps]
build = ["binutils", "ncurses", "libcap", "linux-headers", "make"]