El primer build confirmó la predicción que la propia receta traía escrita: `link = "static"` salió
DINÁMICO, contra la libncursesw del lab, y moría con `Error relocating ... symbol not found` —o sea,
habría arrancado en el sandbox de Alpine y no dentro de una imagen—. La causa de fondo es que zsh
carga sus módulos con dlopen; por eso va `--disable-dynamic`, que los mete en el binario.
Pero `--disable-dynamic` solo dejaba un zsh sin `=~`:
zsh:1: failed to load module: zsh/regex
zsh:1: -regex-match not available for regex
Dos explicaciones mías fueron falsas antes de la buena, y quedan escritas en la receta porque las dos
son plausibles y costaron un build cada una: (1) «quedan en link=dynamic, hay que sedearlos a static»
—el sed no cambió nada, los disponibles ya salían static—; (2) «el .mdd evalúa $ac_cv_func_regcomp y
config.status no tiene esas variables, hay que exportarlas» —el configure contesta yes a las cuatro y
el módulo seguía fuera—.
Lo zanjó `configure.ac`, que no opina: con dynamic apagado, un módulo que su .mdd declara `dynamic`
se DESCARTA (link=no) y sólo los que declaran `either` pasan a estático. regex.mdd dice `dynamic` y
complete.mdd dice `either`: por eso había completado y no había `=~`. El arreglo va sobre los .mdd,
antes del configure, y sobrevive a que make regenere config.modules.
Además `--enable-pcre` era una etiqueta, no un hecho: zsh 5.9 sólo habla PCRE1 y el corpus tiene
pcre2; el configure lo decía tres veces. Se quitan la bandera y la dep, que mentían sobre el cierre.
Y el control va EN LA RECETA: la instalación corta si el ELF tiene INTERP o si falta alguno de
{regex, complete, zle, parameter} dentro del binario. Ese guardián es quien atajó los dos intentos
fallidos en vez de sellarlos. Medido sobre el artefacto: statically linked, 16 módulos cargan,
`=~` contesta, 1663 completadores, 14 M.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
174 lines
9.5 KiB
TOML
174 lines
9.5 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 install.info
|
|
rm -rf "/out"/usr/share/locale
|
|
# `/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"]
|