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…

⎇ 9b5861d815cfb08418773920067400bd11f9aa66 scripts/servidor/jaula-herramientas.sh Shell · 265 líneas · 15.9 KB Historia crudo

jaula: el envoltorio de bwrap entra al generador, no a mano 9b5861d81 · Sergio ·

1#!/bin/sh
2# Arma el `bin/` de la persona con las herramientas del CORPUS, para que el agente enjaulado tenga
3# toolchain sin depender del gestor de paquetes de la imagen.
4#
5# ── POR QUÉ NO SE INSTALAN CON PACMAN (2026-09-18) ──────────────────────────────────────────────
6# El agente de adentro reportó que le faltan `cargo rustc make pkg-config python3`, y la salida
7# obvia —sumarlos a `packages` y `takana qorpa provision`— **hoy no funciona**: con un mapa de un
8# solo id, `pacman` no puede chownear su directorio de descarga y muere en
9#
10#     error: failed to chown temporary download directory …: Invalid argument
11#
12# que es exactamente la consecuencia que el aviso de qorpa viene anunciando (SDD 28 §6.55).
13#
14# Pero no hace falta pacman: **las herramientas ya están selladas en el store**, son musl estáticas y
15# corren igual dentro de una imagen glibc — comprobado con `takana` y con `rustc`. Usarlas es además
16# lo coherente con la distro: el corpus es el que provee, no Arch.
17#
18# `go` y `zig` no van acá: viven en el lab (`/work/dev-fs/tools`) y el lanzador ya los pone en PATH.
19#
20# ── TRES DE LAS DIEZ NO SON ESTÁTICAS, Y ADENTRO NO ARRANCAN (2026-09-18, medido) ───────────────
21# `rustc`, `cargo` y `python3` son musl **dinámicas**. Su `PT_INTERP` pide `/lib/ld-musl-x86_64.so.1`
22# y **la imagen de Arch no lo tiene**, así que adentro las tres mueren con
23#
24#     …/usr/bin/cargo: cannot execute: required file not found
25#
26# `LD_LIBRARY_PATH` no puede taparlo: actúa DESPUÉS de que el cargador cargue, y acá el que falta es
27# el cargador. Por eso este guion pasó en verde la primera vez: se corrió EN LA CAJA, que es musl y
28# lo tiene nativo. El control decía la verdad sobre el sitio equivocado.
29#
30# La cura es invocar el cargador del lab explícitamente. Y el envoltorio de `rustc` tiene que
31# llamarse `rustc` **en el PATH**, no sólo existir: cargo lanza a `rustc` POR NOMBRE, y sin eso muere
32# con `could not execute process … rustc -vV (never executed)`, que no nombra la causa.
33#
34# ── Y `cc` TIENE QUE FILTRAR EL TRIPLE QUE LE PASA cc-rs (2026-09-18, medido) ───────────────────
35# `zig cc` a secas no sirve de `cc` para cargo. cc-rs le pasa SU triple y zig no lo entiende:
36#
37#     $ zig cc --target=x86_64-unknown-linux-musl -o h h.c
38#     error: unable to parse target query 'x86_64-unknown-linux-musl': UnknownOperatingSystem
39#
40# Eso tumba `blake3`, que cuelga de medio árbol de tawasuyu. Es el MISMO defecto que tiene el `cc`
41# de `cc-por-zig.sh` en la caja, arreglado allá igual que acá.
42#
43# ── Y LAS BANDERAS DE ENLACE VAN EN EL ENVOLTORIO, NO EN /etc/profile.d ─────────────────────────
44# En la caja las pone `cc-por-zig.sh` en `/etc/profile.d/rust-enlazador.sh`. Acá eso NO serviría: la
45# jaula no corre shells de login —el lanzador hace `exec` directo— así que el fichero quedaría
46# presente, con contenido, y sin efecto: justo la avería que este repo persigue. Van adentro del
47# envoltorio de `rustc`, que es el único sitio por el que pasan TODAS las compilaciones, incluidas
48# las de los build scripts (a los que `RUSTFLAGS` no alcanza cuando se pasa `--target`).
49# Van PRIMERO para que lo que ponga la persona en `RUSTFLAGS` gane: en rustc, la última `-C` manda.
50set -eu
51
52U=${1:-sergio}
53BIN=${BIN:-/work/$U/bin}
54install -d -o "$U" -g "$U" "$BIN"
55
56# ── El lab: de ahí sale el cargador musl que la imagen ajena no tiene ───────────────────────────
57LAB=${TAKANA_LAB:-}
58if [ -z "$LAB" ]; then
59  for d in /work/dev-fs/alpine "/work/$U/dev-fs/alpine"; do
60    [ -x "$d/lib/ld-musl-x86_64.so.1" ] && { LAB=$d; break; }
61  done
62fi
63[ -n "$LAB" ] || { echo "no encuentro el alpine del lab (probé /work/dev-fs/alpine y /work/$U/dev-fs/alpine)" >&2; exit 1; }
64echo "==> lab en $LAB"
65
66# ── Y EL CARGADOR VA TAMBIÉN EN `/lib`, QUE NO ES REDUNDANTE (2026-09-21, medido) ───────────────
67# El envoltorio alcanza para que `cargo` ARRANQUE, pero no para lo que cargo lanza por su cuenta:
68# cargo exporta `CARGO=<ruta del binario REAL>` —el del store, no el envoltorio— y hay macros que
69# lo ejecutan. `proc-macro-crate` corre `$CARGO locate-project` para ubicar la raíz del workspace,
70# y adentro de la jaula ese exec moría con «No such file or directory» —que acá miente: el fichero
71# existe y es ejecutable, el que falta es el INTÉRPRETE—. `crate_name()` devolvía Err y el derive
72# `Type` de zvariant caía a su última rama (`::zvariant`) en vez de `::zbus::zvariant`:
73#
74#     error[E0433]: cannot find `zvariant` in the crate root      (atspi-proxies 0.13.0)
75#
76# Eso se lee como un defecto del lock del repo ajeno y NO lo es: con el cargador puesto,
77# `llimphi-ui` de tawasuyu —winit + accesskit + wgpu— compila entero en 2m17s. La clase es más
78# grande que este caso: CUALQUIER herramienta que re-ejecute `$CARGO` o `$RUSTC` por su ruta
79# absoluta se rompía igual, en un crate ajeno y a diez capas del origen.
80if [ ! -e /lib/ld-musl-x86_64.so.1 ] && ln -sfn "$LAB/lib/ld-musl-x86_64.so.1" /lib/ld-musl-x86_64.so.1 2>/dev/null; then
81  echo "  (puesto /lib/ld-musl-x86_64.so.1 -> $LAB — sin él, lo que cargo LANZA no arranca)"
82fi
83
84# El zig del CORPUS, el mismo que usa cc-por-zig.sh en la caja. No se toma el de `/work/dev-fs/tools`
85# para que el `cc` de adentro y el de afuera sean el mismo binario.
86ZIG=$(find /store -maxdepth 3 -type f -perm -100 -name zig -path '*seed-zig*' 2>/dev/null | head -1)
87[ -n "$ZIG" ] || { echo "no encuentro el zig de seed-zig en /store — sin él no hay driver de C" >&2; exit 1; }
88echo "==> zig $("$ZIG" version) en $ZIG"
89
90# Cuerpo común de los envoltorios. $1 = nombre del binario, $2 = nombre de la RECETA, $3 = extra.
91#
92# ⚠ El nombre de la receta se compara ENTERO, no por sufijo: el glob `/store/*-make` también casa
93# con `…-cargo-make` —medido: la primera versión eligió cargo-make y `make --version` moría con un
94# `exec` que no nombraba la causa—. El directorio es `<hash>-<receta>`: se le quita el hash y se
95# compara. Y se elige el MÁS RECIENTE, no un hash cableado, para sobrevivir a un re-sellado.
96busca_artefacto() {
97  echo "b=\$(ls -dt /store/*-$1 2>/dev/null | while read -r d; do n=\${d##*/}; [ \"\${n#*-}\" = \"$1\" ] && { echo \"\$d\"; break; }; done)"
98  echo "[ -n \"\$b\" ] || { echo '$2: no hay artefacto -$1 en /store' >&2; exit 1; }"
99}
100
101envoltorio() {
102  t=$1; r=$2; pre=$3
103  {
104    echo '#!/bin/sh'
105    echo "# Envoltorio: $t sale del store (musl estático, corre dentro de la imagen glibc)."
106    busca_artefacto "$r" "$t"
107    echo "$pre"
108    echo "exec \"\$b/usr/bin/$t\" \"\$@\""
109  } > "$BIN/$t"
110  chmod 755 "$BIN/$t"
111}
112
113# Las dinámicas: por el cargador del lab, y sólo si la caja no tiene el suyo. Así este mismo guion
114# sirve en la caja (musl, cargador nativo) y en la jaula (Arch, sin cargador musl) sin ramificar.
115# $1 = binario, $2 = receta, $3 = banderas que el envoltorio antepone a los argumentos.
116envoltorio_dinamico() {
117  t=$1; r=$2; flags=$3
118  {
119    echo '#!/bin/sh'
120    echo "# Envoltorio: $t sale del store. Es musl DINÁMICA: sin el cargador no arranca (ver cabecera)."
121    busca_artefacto "$r" "$t"
122    echo "L=\"$LAB/lib:$LAB/usr/lib:\$b/usr/lib\""
123    echo 'if [ -x /lib/ld-musl-x86_64.so.1 ]; then'
124    echo "  LD_LIBRARY_PATH=\"\$L\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}\" exec \"\$b/usr/bin/$t\" $flags \"\$@\""
125    echo 'else'
126    echo "  exec \"$LAB/lib/ld-musl-x86_64.so.1\" --library-path \"\$L\" \"\$b/usr/bin/$t\" $flags \"\$@\""
127    echo 'fi'
128  } > "$BIN/$t"
129  chmod 755 "$BIN/$t"
130}
131
132for par in make:make pkgconf:pkgconf jq:jq rsync:rsync curl:curl takana:takana; do
133  envoltorio "${par%%:*}" "${par##*:}" ":"
134done
135
136# ── `git` aparte: SU EXEC-PATH ESTÁ COMPILADO Y ES ABSOLUTO (medido el 2026-09-22) ──────────────
137# `git` no es un binario: es un multiplexor que lanza `git-remote-https`, `git-http-fetch` y otros
138# treinta ayudantes buscándolos en un directorio COMPILADO AL BUILD — `/usr/libexec/git-core`, la ruta
139# FHS. Corrido desde el store, esa ruta es la de la IMAGEN de la jaula, que acá está VACÍA de
140# ayudantes (`ls /usr/libexec/git-core | grep -c remote-http` ⇒ **0**), así que git no encuentra los
141# SUYOS aunque estén en su artefacto:
142#
143#     $ git ls-remote https://github.com/…
144#     git: 'remote-https' is not a git command.
145#
146# ⚠ **Y eso NO es «el git del corpus no trae los helpers», que es lo que este repo creía y escribió en
147# CLAUDE.md §2.** Los trae: `…-git/usr/libexec/git-core/git-remote-https` está ahí, medido. Lo único
148# que faltaba era decirle dónde mirar. Con `GIT_EXEC_PATH` apuntando a su propio libexec, el mismo
149# binario clona por https Y empuja al espejo de GitHub — comprobado con `ls-remote` y con
150# `push --dry-run`, que llegó hasta el rechazo por non-fast-forward, o sea que habló TLS y autenticó.
151#
152# Es la misma familia que el resto de este guion (un binario del store que no encuentra lo suyo
153# porque la ruta está horneada), sólo que acá lo que falta no es el cargador: es un directorio.
154envoltorio git git 'export GIT_EXEC_PATH="$b/usr/libexec/git-core"'
155
156# ── `bwrap` aparte: NO ES UNA COMODIDAD, ES LA RECUPERACIÓN DE UNA JAULA A MEDIAS ───────────────
157# Los demás envoltorios de este guion existen para traer una herramienta buena del corpus. Éste
158# existe porque **la jaula viva pierde el CONTENIDO de `/usr/bin` y `/bin`** — pasó dos veces el
159# 2026-10-20 — y el síntoma no se parece en nada a la causa:
160#
161#     ls /usr/bin | wc -l   ⇒ 0        pero  /bin/bash --version  ⇒ FUNCIONA
162#
163# O sea que el `readdir` devuelve vacío y el `lookup` por nombre sigue resolviendo: lo que ya estaba
164# en uso anda, y lo que hay que buscar por `PATH` no. Con eso se va el `bwrap` de la imagen, y
165# entonces `takana build` muere así:
166#
167#     Error: spawn bwrap: No such file or directory (os error 2)
168#
169# que se lee como un takana roto o un paquete que falta. **No falta nada**: `bubblewrap` está en el
170# store, sellado, y el binario del store aísla de verdad (comprobado con `--unshare-pid`). Lo que
171# falta es el camino hasta él — misma figura que el `GIT_EXEC_PATH` de arriba, donde el git del
172# corpus SÍ traía sus ayudantes. *Antes de creer que falta un paquete, comprobar si lo que falta es
173# el camino.*
174#
175# Va acá, en el generador, y no a mano en `/work/sergio/bin`: un arreglo que vive fuera del repo se
176# pierde al rehacer la jaula, que es exactamente lo que pasó con los symlinks de `work_root` el
177# 21-sep. Un arreglo que hay que recordar no es un arreglo.
178envoltorio bwrap bwrap ':'
179
180
181# `python3` y `cargo` no llevan banderas; `rustc` sí — son las tres que necesita el enlace en este
182# triple, las mismas que `cc-por-zig.sh` pone en la caja. `link-self-contained=no` porque este
183# toolchain no trae `rust-lld` (incompatible con el llvm-config externo).
184envoltorio_dinamico python3 python3 ""
185envoltorio_dinamico cargo    rust    ""
186envoltorio_dinamico rustc    rust    "-C linker=$BIN/cc -C linker-flavor=gcc -C link-self-contained=no"
187
188# ── El driver de C ─────────────────────────────────────────────────────────────────────────────
189# La caché de zig NO va en el home: son varios G. Misma razón que en cc-por-zig.sh.
190CACHE=${CACHE:-/work/$U/.cache/zig}
191install -d -o "$U" -g "$U" "$CACHE" 2>/dev/null || mkdir -p "$CACHE"
192for sub in cc c++; do
193  nombre=$sub; [ "$sub" = "c++" ] && nombre=c++
194  cat > "$BIN/$nombre" <<EOF
195#!/bin/sh
196# Envoltorio de \`zig $sub\` como driver de C de la jaula.
197# ⚠ Se DESCARTA el \`--target=\` que pasa cc-rs: usa triples de rustc (\`x86_64-unknown-linux-musl\`) y
198# zig sólo entiende los suyos (\`x86_64-linux-musl\`). Sin esto, \`blake3\` no compila. Medido.
199args=""
200for a in "\$@"; do
201  case "\$a" in --target=*|-target) continue ;; esac
202  args="\$args '\$a'"
203done
204export ZIG_GLOBAL_CACHE_DIR="\${ZIG_GLOBAL_CACHE_DIR:-$CACHE}"
205eval exec "$ZIG" $sub -target x86_64-linux-musl \$args
206EOF
207  chmod 755 "$BIN/$nombre"
208done
209
210chown -R "$U:$U" "$BIN" 2>/dev/null || :
211
212# ── CONTROL: que CORRAN, no que existan ─────────────────────────────────────────────────────────
213# ⚠ Sin tubería: `cmd | head` devuelve el estado de `head`, o sea SIEMPRE 0, y la primera versión de
214# este control dio por bueno un envoltorio que fallaba al `exec`. Se captura primero, se recorta después.
215fallos=0
216for t in make pkgconf python3 jq rsync git curl takana rustc cargo; do
217  if out=$("$BIN/$t" --version 2>&1); then
218    printf "  ✓ %-9s %s\n" "$t" "$(printf '%s' "$out" | head -1 | cut -c1-46)"
219  else
220    printf "  ✗ %-9s %s\n" "$t" "$(printf '%s' "$out" | head -1 | cut -c1-60)"; fallos=$((fallos+1))
221  fi
222done
223
224# ── CONTROL: que COMPILEN, que es otra cosa ────────────────────────────────────────────────────
225# `--version` no toca ni el driver de C ni el enlazador, así que no habría visto NINGUNO de los dos
226# defectos de arriba. Estos tres sí, y ninguno necesita red:
227#   1. C con el `--target=` de cc-rs  → el defecto que tumbaba blake3
228#   2. una proc-macro                 → el enlace de dylibs, que es lo que rompía sin las banderas
229#   3. un binario estático            → que el remedio no sea peor que la enfermedad
230T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
231printf '#include <stdio.h>\nint main(void){ puts("C"); return 0; }\n' > "$T/h.c"
232if "$BIN/cc" --target=x86_64-unknown-linux-musl -o "$T/h" "$T/h.c" 2>"$T/e1" && [ "$("$T/h")" = "C" ]; then
233  printf "  ✓ %-9s con el --target= de cc-rs\n" "cc"
234else
235  printf "  ✗ %-9s %s\n" "cc" "$(head -1 "$T/e1")"; fallos=$((fallos+1))
236fi
237printf 'extern crate proc_macro;\nuse proc_macro::TokenStream;\n#[proc_macro]\npub fn m(_i: TokenStream) -> TokenStream { "fn v() -> u32 { 42 }".parse().unwrap() }\n' > "$T/pm.rs"
238if "$BIN/rustc" --edition 2021 --crate-type proc-macro --out-dir "$T" "$T/pm.rs" 2>"$T/e2" && [ -n "$(ls "$T"/*.so 2>/dev/null)" ]; then
239  printf "  ✓ %-9s proc-macro (dylib) enlazada\n" "rustc"
240else
241  printf "  ✗ %-9s %s\n" "rustc" "$(grep -m1 error "$T/e2" 2>/dev/null || head -1 "$T/e2")"; fallos=$((fallos+1))
242fi
243printf 'fn main(){ println!("estatico"); }\n' > "$T/s.rs"
244if "$BIN/rustc" --edition 2021 -o "$T/s" "$T/s.rs" 2>"$T/e3" && [ "$("$T/s")" = "estatico" ]; then
245  printf "  ✓ %-9s binario estático\n" "rustc"
246else
247  printf "  ✗ %-9s %s\n" "rustc" "$(grep -m1 error "$T/e3" 2>/dev/null || head -1 "$T/e3")"; fallos=$((fallos+1))
248fi
249
250# ── CONTROL: y que arranquen SIN el envoltorio ─────────────────────────────────────────────────
251# Esto es lo que ve una proc-macro: `$CARGO` y `$RUSTC` apuntan al binario del STORE, no a `$BIN`.
252# Los tres controles de arriba pasan con este camino roto —todos entran por el envoltorio—, y el
253# error sale después, en un crate ajeno, diciendo otra cosa.
254R=$(ls -dt /store/*-rust 2>/dev/null | while read -r d; do n=${d##*/}; [ "${n#*-}" = "rust" ] && { echo "$d"; break; }; done)
255export LD_LIBRARY_PATH="$LAB/lib:$LAB/usr/lib:$R/usr/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
256mkdir -p "$T/src"; : > "$T/src/lib.rs"   # sin un target, `locate-project` ni llega a mirar la ruta
257printf '[package]\nname = "x"\nversion = "0.0.0"\nedition = "2021"\n' > "$T/Cargo.toml"
258if out=$("$R/usr/bin/cargo" locate-project --workspace --message-format=plain --manifest-path="$T/Cargo.toml" 2>&1); then
259  printf "  ✓ %-9s el binario del store arranca sin envoltorio (\$CARGO)\n" "cargo"
260else
261  printf "  ✗ %-9s %s\n" "cargo" "$(printf '%s' "$out" | head -1 | cut -c1-60)"; fallos=$((fallos+1))
262fi
263
264[ "$fallos" -eq 0 ] || { echo "==> $fallos comprobación(es) en rojo" >&2; exit 1; }
265echo "==> $BIN listo"