# 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"]