From 48b2152c24b4d449cb21e2d65cea3051c4f49c3c Mon Sep 17 00:00:00 2001 From: Sergio Date: Mon, 21 Sep 2026 01:35:33 +0000 Subject: [PATCH] =?UTF-8?q?jaula:=20el=20sysroot=20del=20corpus=20?= =?UTF-8?q?=E2=80=94=20el=20compositor=20ya=20se=20construye=20y=20CORRE?= =?UTF-8?q?=20adentro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `jaula-herramientas.sh` deja el compilador, y eso alcanza hasta que algo toca una biblioteca de C. `mirada-compositor` moría en su build script con «The PKG_CONFIG_PATH environment variable is not set», y el mensaje engaña: las dieciocho bibliotecas están en el store. Lo que no había era dónde verlas juntas — los `.pc` del corpus declaran `prefix=/usr` porque se miran desde el sandbox de `takana build`, que monta la clausura ahí. La lista sale de `recipes/mirada-compositor.toml`, no de una copia que se pudra. Se le suman dos cosas que ninguna receta nombra y que un sysroot a mano sí necesita: `musl` (adentro del sandbox la libc la pone el sandbox; sin sus cabeceras bindgen lee las de glibc y muere en 'stddef.h' file not found) y las gemelas `-shared` cuando existen (la receta pide zlib/expat/libffi estáticas, pero el producto es dinámico y al CORRER pide las .so). Tres cosas que sólo aparecieron haciéndolo: · La granja espeja el ÁRBOL ENTERO, no `/usr`. El primer intento perdía `linux-pam` completo, que instala en /lib64 — y no falló compilando: falló con el binario ya enlazado tomando el libpam de Arch y muriendo en __memcpy_chk. · Los `-L` van explícitos y derivados de la granja. pkg-config sólo emite el del módulo que le preguntan, y `pam-sys` no usa pkg-config: emite `link-lib=pam` y nada más. Con el sysroot a medias eso daba verde porque lld caía al /usr/lib de Arch; con el sysroot completo dice la verdad y no enlaza. · El control corre `xkb_keysym_from_name` y no `xkb_context_new`: el contexto necesita los datos de xkeyboard-config, que no están en la clausura, y el control moría por algo que no estaba midiendo. Medido adentro de la jaula, con el ambiente limpio (todo sale de la config de cargo): `cargo build -p mirada-compositor` en 5m13s, y el binario arranca, elige el backend DRM y llega hasta donde la jaula lo deja. Sin regresión: `format` compila y los 14 tests de `llimphi-cpu` pasan. --- scripts/servidor/jaula-sysroot.sh | 199 ++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100755 scripts/servidor/jaula-sysroot.sh diff --git a/scripts/servidor/jaula-sysroot.sh b/scripts/servidor/jaula-sysroot.sh new file mode 100755 index 00000000..e39e15df --- /dev/null +++ b/scripts/servidor/jaula-sysroot.sh @@ -0,0 +1,199 @@ +#!/bin/sh +# jaula-sysroot.sh — el sysroot del corpus para compilar DENTRO de la jaula qorpa. +# +# ── QUÉ PROBLEMA RESUELVE ─────────────────────────────────────────────────────────────────────── +# `jaula-herramientas.sh` deja el COMPILADOR (rustc, cargo, cc). Eso alcanza para todo lo que no +# toca una biblioteca de C. Lo que necesita una —el compositor de mirada: DRM, GBM, libseat, +# libinput, xkbcommon, PAM— muere en su build script con: +# +# The PKG_CONFIG_PATH environment variable is not set. +# HINT: if you have installed the library, try setting PKG_CONFIG_PATH ... `libseat.pc` +# +# y el mensaje engaña: las bibliotecas SÍ están, las dieciocho, en el store del corpus. Lo que no +# hay es dónde verlas juntas. Los `.pc` del corpus declaran `prefix=/usr` porque están hechos para +# mirarse desde el sandbox de `takana build`, que monta la clausura ahí (`takana-build/src/harkaq.rs`). +# Fuera de ese sandbox no hay ningún `/usr` que los contenga. +# +# Esto arma ese `/usr`: una granja de enlaces con el árbol de cada artefacto, y `PKG_CONFIG_SYSROOT_DIR` +# para que pkg-config reescriba los `-I`/`-L` hacia la granja en vez de hacia la imagen de Arch. +# +# ── LA LISTA SALE DE LA RECETA, NO DE ACÁ ─────────────────────────────────────────────────────── +# `recipes/mirada-compositor.toml` ya declara sus dieciocho build-deps. Copiarlas sería una segunda +# lista que se pudre sola: se leen de ahí. Se agregan dos cosas que la receta NO nombra, y con razón: +# +# · `musl` — adentro de `takana build` la libc la pone el sandbox, así que ninguna receta la +# declara. Un sysroot armado a mano sí la necesita: sin sus cabeceras, bindgen lee las de +# glibc de la imagen de Arch y muere en `'stddef.h' file not found`, que no nombra la causa. +# · `-shared` cuando existe — la receta pide `zlib`/`expat`/`libffi`, que son estáticas. +# El producto es `link = "dynamic"`: al CORRER pide `libz.so.1`, `libexpat.so.1`, `libffi.so.8`, +# y sin las gemelas dinámicas el cargador cae a las de la imagen y revienta relocando +# (`__snprintf_chk: symbol not found`). Compilar andaba igual; correr, no. +# +# ── LA GRANJA ESPEJA EL ÁRBOL ENTERO, NO `/usr` ──────────────────────────────────────────────── +# El primer intento enlazaba sólo `usr/{lib,include,share}` y perdía `linux-pam` COMPLETO, que +# instala en `/lib64`. El síntoma no fue un error de compilación: fue el binario ya enlazado +# tomando el `libpam.so.0` de Arch y muriendo en `__memcpy_chk`. Se espeja todo. +# +# Medido con esto puesto (2026-09-21, dentro de la jaula): `cargo build -p mirada-compositor` +# enlaza en 4m34s y el binario ARRANCA — 428 MB, intérprete musl, parsea sus argumentos. +# +# Uso: sh scripts/servidor/jaula-sysroot.sh [usuario] [receta...] +set -eu + +U=${1:-$(id -un)}; [ $# -gt 0 ] && shift || true +RECETAS=${*:-recipes/mirada-compositor.toml} +SYS=${SYSROOT:-/work/$U/sysroot} +BIN=${BIN:-/work/$U/bin} +CARGO_HOME=${CARGO_HOME:-/home/$U/.cargo} +STORE=${STORE:-/store} + +[ -x "$BIN/pkgconf" ] || { echo "falta $BIN/pkgconf — corré antes jaula-herramientas.sh" >&2; exit 1; } + +# ── Las dependencias, leídas de la receta ────────────────────────────────────────────────────── +# `build = [...]` puede venir en varias líneas; se junta todo y se sacan los entrecomillados. +deps="" +for r in $RECETAS; do + [ -f "$r" ] || { echo "no existe la receta $r" >&2; exit 1; } + d=$(tr '\n' ' ' < "$r" | sed -n 's/.*build[[:space:]]*=[[:space:]]*\[\([^]]*\)\].*/\1/p' | tr -d '"' | tr ',' ' ') + [ -n "$d" ] || { echo "la receta $r no declara [deps] build" >&2; exit 1; } + deps="$deps $d" + echo "==> $r: $(printf '%s' "$d" | wc -w) build-deps" +done +deps="$deps musl" + +# ── Resolver nombre -> artefacto, y sumar la gemela `-shared` si existe ──────────────────────── +# El sufijo se compara EXACTO: `ls /store/*-musl` también casa `…-libudev-zero-musl` si existiera. +artefacto() { + ls -dt "$STORE"/*-"$1" 2>/dev/null | while read -r d; do + b=${d##*/}; [ "${b#*-}" = "$1" ] && { echo "$d"; break; } + done +} +rutas=""; faltan=""; vistos="" +for n in $deps; do + case " $vistos " in *" $n "*) continue ;; esac + vistos="$vistos $n" + a=$(artefacto "$n") + if [ -n "$a" ]; then rutas="$rutas $a"; else faltan="$faltan $n"; fi + s=$(artefacto "$n-shared"); [ -n "$s" ] && rutas="$rutas $s" +done +[ -z "$faltan" ] || { echo "==> no están en el store:$faltan" >&2; exit 1; } +echo "==> $(printf '%s' "$rutas" | wc -w) artefactos" + +# ── La granja ────────────────────────────────────────────────────────────────────────────────── +# Enlaces, no copias ni hardlinks: `/store` y `/work` son filesystems distintos (medido: sdb y sdc), +# así que `takana hydrate --link static` no puede. Rehacerla entera cuesta menos de un segundo. +rm -rf "$SYS"; mkdir -p "$SYS" +for a in $rutas; do + ( cd "$a" && find . -mindepth 1 -type d ! -path './.hammer*' -printf '%P\n' ) | while read -r d; do + mkdir -p "$SYS/$d" + done + ( cd "$a" && find . -mindepth 1 ! -type d ! -path './.hammer*' -printf '%P\n' ) | while read -r f; do + [ -e "$SYS/$f" ] || ln -s "$a/$f" "$SYS/$f" + done +done +echo "==> $SYS: $(find "$SYS" -type l | wc -l) enlaces" + +# ── El nombre `pkg-config`, que el corpus no trae ────────────────────────────────────────────── +# El binario del corpus se llama `pkgconf`; el crate `pkg-config` de Rust lanza `pkg-config`. Sin +# este alias el build script no encuentra NADA y lo dice como si faltara la biblioteca. +printf '#!/bin/sh\nexec %s/pkgconf "$@"\n' "$BIN" > "$BIN/pkg-config" +chmod 755 "$BIN/pkg-config" + +# ── El ambiente, en la config de cargo y no en el shell ──────────────────────────────────────── +# `[env]` de cargo llega a los build scripts, que es donde hace falta. Si fuera una variable de +# shell habría que acordarse en cada sesión, y el olvido se lee como «falta la biblioteca». +# ⚠ NO se pone `LD_LIBRARY_PATH` acá: el envoltorio de cargo ya exporta el suyo (lab + rust) y +# pisarlo deja a rustc sin sus dylibs. Para CORRER un binario dinámico va en la línea, y el guion +# la imprime al final. +PCP="$SYS/usr/lib/pkgconfig:$SYS/usr/share/pkgconfig:$SYS/lib64/pkgconfig:$SYS/lib/pkgconfig" + +# ── Los `-L`, que NO los pone pkg-config ─────────────────────────────────────────────────────── +# pkg-config emite el `-L` del módulo que le preguntan, y eso deja afuera a los que se enlazan sin +# `.pc`: `pam-sys` sólo emite `cargo:rustc-link-lib=pam`, sin ruta. Con el sysroot a medias eso +# "andaba" porque lld caía al `/usr/lib` de Arch y tomaba el libpam de glibc — o sea que el enlace +# salía verde y el binario moría al arrancar. Con el sysroot completo ya no cae a ningún lado y +# dice la verdad: `unable to find dynamic system library 'pam'`. Así que las rutas de biblioteca +# van explícitas, y se DERIVAN de la granja: el corpus no instala todo en el mismo sitio. +# ⚠ `rustflags` no se fusiona entre configs: si un repo define el suyo, éste se ignora ENTERO y +# el sysroot deja de verse. Hoy tawasuyu sólo define `[env]`, así que convive. +# Sólo lo que un `-l` puede nombrar (`lib.so`/`.a`): así quedan afuera `lib64/security` y +# `usr/lib/dri`, que son módulos que alguien abre con dlopen, no bibliotecas que se enlacen. +LIBDIRS=$(find "$SYS" \( -name 'lib*.so' -o -name 'lib*.so.*' -o -name 'lib*.a' \) | sed 's#/[^/]*$##' | sort -u) + +ZIG=$(find "$STORE" -maxdepth 3 -type f -perm -100 -name zig -path '*seed-zig*' 2>/dev/null | head -1) +[ -n "$ZIG" ] || { echo "no encuentro el zig de seed-zig en $STORE" >&2; exit 1; } +ZL="${ZIG%/zig}/lib" +BCA="--target=x86_64-linux-musl --sysroot=$SYS -I$ZL/include -I$SYS/usr/include" + +mkdir -p "$CARGO_HOME" +CFG="$CARGO_HOME/config.toml" +MARCA="# >>> jaula-sysroot.sh" +[ -f "$CFG" ] || : > "$CFG" +awk -v m="$MARCA" 'index($0,m)==1{s=1} !s{print} index($0,"# <<< jaula-sysroot.sh")==1{s=0}' "$CFG" > "$CFG.tmp" +{ + cat "$CFG.tmp" + echo "$MARCA — NO editar a mano, se regenera" + echo '[env]' + echo "PKG_CONFIG_PATH = \"$PCP\"" + echo "PKG_CONFIG_SYSROOT_DIR = \"$SYS\"" + echo "BINDGEN_EXTRA_CLANG_ARGS = \"$BCA\"" + echo '[build]' + printf 'rustflags = [' + sep="" + for d in $LIBDIRS; do printf '%s"-L", "%s"' "$sep" "$d"; sep=", "; done + echo ']' + echo '# <<< jaula-sysroot.sh' +} > "$CFG" +rm -f "$CFG.tmp" +echo "==> $CFG" + +# ── CONTROL: que TODOS los .pc de la granja resuelvan ────────────────────────────────────────── +# Es lo que ve el build script. Un PKG_CONFIG_PATH a medias no da error acá: da «no está instalada». +fallos=0 +export PKG_CONFIG_PATH="$PCP" PKG_CONFIG_SYSROOT_DIR="$SYS" +total=0; rotos="" +for pc in $(find "$SYS" -name '*.pc' -printf '%f\n' | sed 's/\.pc$//' | sort -u); do + total=$((total+1)) + "$BIN/pkgconf" --exists "$pc" 2>/dev/null || rotos="$rotos $pc" +done +# ⚠ Que NO resuelvan todos es normal y no es rojo: un artefacto trae también los `.pc` de sus +# features opcionales (`libxkbcommon` trae `xkbcommon-x11.pc`, que pide X11 y no está en esta +# clausura). Rojo es que no resuelva NINGUNO — eso sí es el PKG_CONFIG_PATH roto. +ok=$((total - $(printf '%s' "$rotos" | wc -w))) +if [ "$ok" -gt 0 ]; then + printf " ✓ %-11s %s de %s módulos resuelven\n" "pkg-config" "$ok" "$total" + [ -z "$rotos" ] || echo " · sin resolver (features opcionales fuera de la clausura):$rotos" +else + printf " ✗ %-11s no resuelve NINGUNO de %s — PKG_CONFIG_PATH roto\n" "pkg-config" "$total"; fallos=$((fallos+1)) +fi + +# ── CONTROL: que COMPILE, ENLACE y CORRA contra una del sysroot ──────────────────────────────── +# `--exists` sólo mira el `.pc`. Esto atraviesa los `-I`, los `-L`, el enlazador y el cargador —que +# es donde estuvieron los dos defectos: las cabeceras de glibc y el `libpam.so.0` de la imagen. +if "$BIN/pkgconf" --exists xkbcommon 2>/dev/null; then + T=$(mktemp -d); trap 'rm -rf "$T"' EXIT + # `xkb_keysym_from_name` a propósito, y no `xkb_context_new`: el contexto necesita los datos de + # xkeyboard-config en /usr/share/X11/xkb, que no están en la clausura, y el control moría por eso + # con el sysroot perfecto. Un control que falla por algo que no mide es peor que no tenerlo. + printf '#include \n#include \nint main(void){ return xkb_keysym_from_name("a",XKB_KEYSYM_NO_FLAGS)==0x61 ? (puts("ok"),0) : 1; }\n' > "$T/x.c" + if "$BIN/cc" $("$BIN/pkgconf" --cflags --libs xkbcommon) -o "$T/x" "$T/x.c" 2>"$T/e" \ + && [ "$(LD_LIBRARY_PATH="$SYS/usr/lib:$SYS/lib64" "$T/x" 2>&1)" = "ok" ]; then + # Y de DÓNDE salió la .so: si el enlazador la tomó de la imagen de Arch el programa corre igual + # y el sysroot no sirvió para nada. Eso es exactamente lo que pasó con `libpam.so.0`. + 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') + case "$de" in + "$SYS"/*) printf " ✓ %-11s compila, enlaza y corre — la .so sale del sysroot\n" "xkbcommon" ;; + "") printf " ✗ %-11s enlazó, pero no pude ver de dónde sale la .so\n" "xkbcommon"; fallos=$((fallos+1)) ;; + *) printf " ✗ %-11s la .so sale de %s, NO del sysroot\n" "xkbcommon" "$de"; fallos=$((fallos+1)) ;; + esac + else + printf " ✗ %-11s %s\n" "xkbcommon" "$(head -1 "$T/e")"; fallos=$((fallos+1)) + fi +else + echo " · xkbcommon no está en esta granja: me salto el control de enlace" +fi + +[ "$fallos" -eq 0 ] || { echo "==> $fallos comprobación(es) en rojo" >&2; exit 1; } +LLP=$(printf '%s\n' $LIBDIRS | tr '\n' ':' | sed 's/:$//') +echo "==> listo. Para CORRER un binario dinámico:" +echo " LD_LIBRARY_PATH=$LLP ./target/debug/"