tawasuyu

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…

TOML 1182Shell 87Rust 63Markdown 541592 Definiciones

⎇ 93c035f15f8f8aab331a99650e173b325ebf91b0 scripts/servidor/jaula-sysroot.sh Shell · 214 líneas · 13.6 KB Historia crudo

fix(jaula): el sysroot no traía el paquete de la propia receta, y ponía plugins en los `-L` 5728b5ff8 · Sergio ·

1#!/bin/sh
2# jaula-sysroot.sh — el sysroot del corpus para compilar DENTRO de la jaula qorpa.
3#
4# ── QUÉ PROBLEMA RESUELVE ───────────────────────────────────────────────────────────────────────
5# `jaula-herramientas.sh` deja el COMPILADOR (rustc, cargo, cc). Eso alcanza para todo lo que no
6# toca una biblioteca de C. Lo que necesita una —el compositor de mirada: DRM, GBM, libseat,
7# libinput, xkbcommon, PAM— muere en su build script con:
8#
9#     The PKG_CONFIG_PATH environment variable is not set.
10#     HINT: if you have installed the library, try setting PKG_CONFIG_PATH ... `libseat.pc`
11#
12# y el mensaje engaña: las bibliotecas SÍ están, las dieciocho, en el store del corpus. Lo que no
13# hay es dónde verlas juntas. Los `.pc` del corpus declaran `prefix=/usr` porque están hechos para
14# mirarse desde el sandbox de `takana build`, que monta la clausura ahí (`takana-build/src/harkaq.rs`).
15# Fuera de ese sandbox no hay ningún `/usr` que los contenga.
16#
17# Esto arma ese `/usr`: una granja de enlaces con el árbol de cada artefacto, y `PKG_CONFIG_SYSROOT_DIR`
18# para que pkg-config reescriba los `-I`/`-L` hacia la granja en vez de hacia la imagen de Arch.
19#
20# ── LA LISTA SALE DE LA RECETA, NO DE ACÁ ───────────────────────────────────────────────────────
21# `recipes/mirada-compositor.toml` ya declara sus dieciocho build-deps. Copiarlas sería una segunda
22# lista que se pudre sola: se leen de ahí. Se agregan dos cosas que la receta NO nombra, y con razón:
23#
24#   · `musl` — adentro de `takana build` la libc la pone el sandbox, así que ninguna receta la
25#     declara. Un sysroot armado a mano sí la necesita: sin sus cabeceras, bindgen lee las de
26#     glibc de la imagen de Arch y muere en `'stddef.h' file not found`, que no nombra la causa.
27#   · `<dep>-shared` cuando existe — la receta pide `zlib`/`expat`/`libffi`, que son estáticas.
28#     El producto es `link = "dynamic"`: al CORRER pide `libz.so.1`, `libexpat.so.1`, `libffi.so.8`,
29#     y sin las gemelas dinámicas el cargador cae a las de la imagen y revienta relocando
30#     (`__snprintf_chk: symbol not found`). Compilar andaba igual; correr, no.
31#
32# ── LA GRANJA ESPEJA EL ÁRBOL ENTERO, NO `/usr` ────────────────────────────────────────────────
33# El primer intento enlazaba sólo `usr/{lib,include,share}` y perdía `linux-pam` COMPLETO, que
34# instala en `/lib64`. El síntoma no fue un error de compilación: fue el binario ya enlazado
35# tomando el `libpam.so.0` de Arch y muriendo en `__memcpy_chk`. Se espeja todo.
36#
37# Medido con esto puesto (2026-09-21, dentro de la jaula): `cargo build -p mirada-compositor`
38# enlaza en 4m34s y el binario ARRANCA — 428 MB, intérprete musl, parsea sus argumentos.
39#
40# Uso:  sh scripts/servidor/jaula-sysroot.sh [usuario] [receta...]
41set -eu
42
43U=${1:-$(id -un)}; [ $# -gt 0 ] && shift || true
44RECETAS=${*:-recipes/mirada-compositor.toml}
45SYS=${SYSROOT:-/work/$U/sysroot}
46BIN=${BIN:-/work/$U/bin}
47CARGO_HOME=${CARGO_HOME:-/home/$U/.cargo}
48STORE=${STORE:-/store}
49
50[ -x "$BIN/pkgconf" ] || { echo "falta $BIN/pkgconf — corré antes jaula-herramientas.sh" >&2; exit 1; }
51
52# ── Las dependencias, leídas de la receta ──────────────────────────────────────────────────────
53# `build = [...]` puede venir en varias líneas; se junta todo y se sacan los entrecomillados.
54deps=""
55for r in $RECETAS; do
56  [ -f "$r" ] || { echo "no existe la receta $r" >&2; exit 1; }
57  d=$(tr '\n' ' ' < "$r" | sed -n 's/.*build[[:space:]]*=[[:space:]]*\[\([^]]*\)\].*/\1/p' | tr -d '"' | tr ',' ' ')
58  [ -n "$d" ] || { echo "la receta $r no declara [deps] build" >&2; exit 1; }
59  # Y EL PAQUETE DE LA RECETA MISMA. Apuntarle a `recipes/pipewire.toml` y que traiga sus quince
60  # build-deps pero no pipewire es lo contrario de lo que cualquiera espera; se pierde en silencio
61  # porque el resultado igual tiene cientos de enlaces y sólo se nota cuando el `.pc` no resuelve.
62  propio=$(sed -n 's/^name[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' "$r" | head -1)
63  deps="$deps $d $propio"
64  echo "==> $r: $propio + $(printf '%s' "$d" | wc -w) build-deps"
65done
66deps="$deps musl"
67
68# ── Resolver nombre -> artefacto, y sumar la gemela `-shared` si existe ────────────────────────
69# El sufijo se compara EXACTO: `ls /store/*-musl` también casa `…-libudev-zero-musl` si existiera.
70artefacto() {
71  ls -dt "$STORE"/*-"$1" 2>/dev/null | while read -r d; do
72    b=${d##*/}; [ "${b#*-}" = "$1" ] && { echo "$d"; break; }
73  done
74}
75rutas=""; faltan=""; vistos=""
76for n in $deps; do
77  case " $vistos " in *" $n "*) continue ;; esac
78  vistos="$vistos $n"
79  a=$(artefacto "$n")
80  if [ -n "$a" ]; then rutas="$rutas $a"; else faltan="$faltan $n"; fi
81  s=$(artefacto "$n-shared"); [ -n "$s" ] && rutas="$rutas $s"
82done
83[ -z "$faltan" ] || { echo "==> no están en el store:$faltan" >&2; exit 1; }
84echo "==> $(printf '%s' "$rutas" | wc -w) artefactos"
85
86# ── La granja ──────────────────────────────────────────────────────────────────────────────────
87# Enlaces, no copias ni hardlinks: `/store` y `/work` son filesystems distintos (medido: sdb y sdc),
88# así que `takana hydrate --link static` no puede. Rehacerla entera cuesta menos de un segundo.
89rm -rf "$SYS"; mkdir -p "$SYS"
90for a in $rutas; do
91  ( cd "$a" && find . -mindepth 1 -type d ! -path './.hammer*' -printf '%P\n' ) | while read -r d; do
92    mkdir -p "$SYS/$d"
93  done
94  ( cd "$a" && find . -mindepth 1 ! -type d ! -path './.hammer*' -printf '%P\n' ) | while read -r f; do
95    [ -e "$SYS/$f" ] || ln -s "$a/$f" "$SYS/$f"
96  done
97done
98echo "==> $SYS: $(find "$SYS" -type l | wc -l) enlaces"
99
100# ── El nombre `pkg-config`, que el corpus no trae ──────────────────────────────────────────────
101# El binario del corpus se llama `pkgconf`; el crate `pkg-config` de Rust lanza `pkg-config`. Sin
102# este alias el build script no encuentra NADA y lo dice como si faltara la biblioteca.
103printf '#!/bin/sh\nexec %s/pkgconf "$@"\n' "$BIN" > "$BIN/pkg-config"
104chmod 755 "$BIN/pkg-config"
105
106# ── El ambiente, en la config de cargo y no en el shell ────────────────────────────────────────
107# `[env]` de cargo llega a los build scripts, que es donde hace falta. Si fuera una variable de
108# shell habría que acordarse en cada sesión, y el olvido se lee como «falta la biblioteca».
109# ⚠ NO se pone `LD_LIBRARY_PATH` acá: el envoltorio de cargo ya exporta el suyo (lab + rust) y
110# pisarlo deja a rustc sin sus dylibs. Para CORRER un binario dinámico va en la línea, y el guion
111# la imprime al final.
112PCP="$SYS/usr/lib/pkgconfig:$SYS/usr/share/pkgconfig:$SYS/lib64/pkgconfig:$SYS/lib/pkgconfig"
113
114# ── Los `-L`, que NO los pone pkg-config ───────────────────────────────────────────────────────
115# pkg-config emite el `-L` del módulo que le preguntan, y eso deja afuera a los que se enlazan sin
116# `.pc`: `pam-sys` sólo emite `cargo:rustc-link-lib=pam`, sin ruta. Con el sysroot a medias eso
117# "andaba" porque lld caía al `/usr/lib` de Arch y tomaba el libpam de glibc — o sea que el enlace
118# salía verde y el binario moría al arrancar. Con el sysroot completo ya no cae a ningún lado y
119# dice la verdad: `unable to find dynamic system library 'pam'`. Así que las rutas de biblioteca
120# van explícitas, y se DERIVAN de la granja: el corpus no instala todo en el mismo sitio.
121# ⚠ `rustflags` no se fusiona entre configs: si un repo define el suyo, éste se ignora ENTERO y
122# el sysroot deja de verse. Hoy tawasuyu sólo define `[env]`, así que convive.
123# SÓLO los cuatro directorios canónicos de biblioteca, y no «todo directorio que tenga un .so».
124# El filtro por nombre no alcanza: una receta declara en `[deps] build` tanto las bibliotecas contra
125# las que se enlaza como las HERRAMIENTAS que necesita para construirse (perl, python, meson), y
126# esas traen `usr/lib/perl5/5.40.2/.../CORE/libperl.so`, `usr/lib/python3.12/config-*/libpython.so`
127# y hasta `usr/share/gdb/auto-load/`. Ponerlos en `-L` es invitar al enlazador a resolver un
128# símbolo con la biblioteca equivocada, que es un defecto carísimo de diagnosticar. Una biblioteca
129# de verdad vive en uno de estos cuatro; lo anidado es plugin, runtime o dlopen.
130LIBDIRS=""
131for d in "$SYS/lib" "$SYS/lib64" "$SYS/usr/lib" "$SYS/usr/lib64"; do
132  # ⚠ `ls a* b*` sale 2 si UNO de los dos no casa, aunque el otro sí. Con eso `lib64` —que tiene
133  # `libpam.so` y ningún `.a`— quedaba afuera, y con él el libpam que el compositor necesita.
134  [ -d "$d" ] || continue
135  find "$d" -maxdepth 1 \( -name 'lib*.so*' -o -name 'lib*.a' \) 2>/dev/null | grep -q . && LIBDIRS="$LIBDIRS $d"
136done
137
138ZIG=$(find "$STORE" -maxdepth 3 -type f -perm -100 -name zig -path '*seed-zig*' 2>/dev/null | head -1)
139[ -n "$ZIG" ] || { echo "no encuentro el zig de seed-zig en $STORE" >&2; exit 1; }
140ZL="${ZIG%/zig}/lib"
141BCA="--target=x86_64-linux-musl --sysroot=$SYS -I$ZL/include -I$SYS/usr/include"
142
143mkdir -p "$CARGO_HOME"
144CFG="$CARGO_HOME/config.toml"
145MARCA="# >>> jaula-sysroot.sh"
146[ -f "$CFG" ] || : > "$CFG"
147awk -v m="$MARCA" 'index($0,m)==1{s=1} !s{print} index($0,"# <<< jaula-sysroot.sh")==1{s=0}' "$CFG" > "$CFG.tmp"
148{
149  cat "$CFG.tmp"
150  echo "$MARCA — NO editar a mano, se regenera"
151  echo '[env]'
152  echo "PKG_CONFIG_PATH = \"$PCP\""
153  echo "PKG_CONFIG_SYSROOT_DIR = \"$SYS\""
154  echo "BINDGEN_EXTRA_CLANG_ARGS = \"$BCA\""
155  echo '[build]'
156  printf 'rustflags = ['
157  sep=""
158  for d in $LIBDIRS; do printf '%s"-L", "%s"' "$sep" "$d"; sep=", "; done
159  echo ']'
160  echo '# <<< jaula-sysroot.sh'
161} > "$CFG"
162rm -f "$CFG.tmp"
163echo "==> $CFG"
164
165# ── CONTROL: que TODOS los .pc de la granja resuelvan ──────────────────────────────────────────
166# Es lo que ve el build script. Un PKG_CONFIG_PATH a medias no da error acá: da «no está instalada».
167fallos=0
168export PKG_CONFIG_PATH="$PCP" PKG_CONFIG_SYSROOT_DIR="$SYS"
169total=0; rotos=""
170for pc in $(find "$SYS" -name '*.pc' -printf '%f\n' | sed 's/\.pc$//' | sort -u); do
171  total=$((total+1))
172  "$BIN/pkgconf" --exists "$pc" 2>/dev/null || rotos="$rotos $pc"
173done
174# ⚠ Que NO resuelvan todos es normal y no es rojo: un artefacto trae también los `.pc` de sus
175# features opcionales (`libxkbcommon` trae `xkbcommon-x11.pc`, que pide X11 y no está en esta
176# clausura). Rojo es que no resuelva NINGUNO — eso sí es el PKG_CONFIG_PATH roto.
177ok=$((total - $(printf '%s' "$rotos" | wc -w)))
178if [ "$ok" -gt 0 ]; then
179  printf "  ✓ %-11s %s de %s módulos resuelven\n" "pkg-config" "$ok" "$total"
180  [ -z "$rotos" ] || echo "    · sin resolver (features opcionales fuera de la clausura):$rotos"
181else
182  printf "  ✗ %-11s no resuelve NINGUNO de %s — PKG_CONFIG_PATH roto\n" "pkg-config" "$total"; fallos=$((fallos+1))
183fi
184
185# ── CONTROL: que COMPILE, ENLACE y CORRA contra una del sysroot ────────────────────────────────
186# `--exists` sólo mira el `.pc`. Esto atraviesa los `-I`, los `-L`, el enlazador y el cargador —que
187# es donde estuvieron los dos defectos: las cabeceras de glibc y el `libpam.so.0` de la imagen.
188if "$BIN/pkgconf" --exists xkbcommon 2>/dev/null; then
189  T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
190  # `xkb_keysym_from_name` a propósito, y no `xkb_context_new`: el contexto necesita los datos de
191  # xkeyboard-config en /usr/share/X11/xkb, que no están en la clausura, y el control moría por eso
192  # con el sysroot perfecto. Un control que falla por algo que no mide es peor que no tenerlo.
193  printf '#include <xkbcommon/xkbcommon.h>\n#include <stdio.h>\nint main(void){ return xkb_keysym_from_name("a",XKB_KEYSYM_NO_FLAGS)==0x61 ? (puts("ok"),0) : 1; }\n' > "$T/x.c"
194  if "$BIN/cc" $("$BIN/pkgconf" --cflags --libs xkbcommon) -o "$T/x" "$T/x.c" 2>"$T/e" \
195     && [ "$(LD_LIBRARY_PATH="$SYS/usr/lib:$SYS/lib64" "$T/x" 2>&1)" = "ok" ]; then
196    # Y de DÓNDE salió la .so: si el enlazador la tomó de la imagen de Arch el programa corre igual
197    # y el sysroot no sirvió para nada. Eso es exactamente lo que pasó con `libpam.so.0`.
198    de=$(LD_LIBRARY_PATH="$SYS/usr/lib:$SYS/lib64" /lib/ld-musl-x86_64.so.1 --list "$T/x" 2>/dev/null | sed -n 's/.*=> *\([^ ]*libxkbcommon[^ ]*\).*/\1/p')
199    case "$de" in
200      "$SYS"/*) printf "  ✓ %-11s compila, enlaza y corre — la .so sale del sysroot\n" "xkbcommon" ;;
201      "")       printf "  ✗ %-11s enlazó, pero no pude ver de dónde sale la .so\n" "xkbcommon"; fallos=$((fallos+1)) ;;
202      *)        printf "  ✗ %-11s la .so sale de %s, NO del sysroot\n" "xkbcommon" "$de"; fallos=$((fallos+1)) ;;
203    esac
204  else
205    printf "  ✗ %-11s %s\n" "xkbcommon" "$(head -1 "$T/e")"; fallos=$((fallos+1))
206  fi
207else
208  echo "  · xkbcommon no está en esta granja: me salto el control de enlace"
209fi
210
211[ "$fallos" -eq 0 ] || { echo "==> $fallos comprobación(es) en rojo" >&2; exit 1; }
212LLP=$(printf '%s\n' $LIBDIRS | tr '\n' ':' | sed 's/:$//')
213echo "==> listo. Para CORRER un binario dinámico:"
214echo "    LD_LIBRARY_PATH=$LLP ./target/debug/<bin>"