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 1572Shell 203Rust 109Markdown 993728 Definitions

⎇ a39110b50bd71d18b33b1a42fca46f82824f79ce scripts/instalador-variantes.sh Shell · 196 lines · 12.8 KB History raw

todo lo publicado va sin símbolos, y GNOME entra: full pasa de 17 a 8,6 GiB con CINCO escritorios d3935a3fa · Sergio ·

1#!/bin/sh
2# instalador-variantes.sh — las tres variantes del instalador, en sus dos medios.
3#
4# ══ QUÉ SON ═══════════════════════════════════════════════════════════════════════════════════
5#   base      sólo el piso de arranque: arranca a una consola y nada más
6#   servidor  el piso de arranque + el userland base + el perfil `servidor`
7#   mirada    el piso de arranque + el userland base + el escritorio propio de la suite
8#   full      el userland, el toolbox (`cli`), los servicios y **los cinco escritorios** —mirada,
9#             sway, cosmic, gnome y kde—. Pide un pendrive de 32 GB.
10#
11# ── LA COMPOSICIÓN, Y POR QUÉ NO ES UN PERFIL SOLO ────────────────────────────────────────────
12# ⚠ **«base» son DOS cosas con el mismo nombre**, y confundirlas da un rootfs que no arranca:
13#
14#   · el **piso de ARRANQUE** — `product-rootfs`: arje-zero como PID 1 + busybox. Es lo único que
15#     hace que la imagen encienda, y NO está en ningún perfil de `targets.toml`.
16#   · el **userland base** — `[perfil.base]`: bash, git, sudo, dhcpcd, coreutils… 359 binarios y
17#     **ni busybox ni arje-zero adentro** (medido: `ls base/bin` no los tiene).
18#
19# El [SDD 27 §1](../docs/27-perfiles-instalables-y-el-instalador.md) ya había medido que los
20# escritorios NO heredan `base`; esto es la otra mitad del mismo hueco. Cada variante se compone
21# entonces así, y en este orden:
22#
23#     product-rootfs  ∪  perfil.base  ∪  <los perfiles de la variante>
24#
25# ── DOS MONTAJES, DOS COPIAS, Y NO ES DESPERDICIO ─────────────────────────────────────────────
26# `hydrate` proyecta por HARDLINK y `linkat()` no cruza un punto de montaje: la composición tiene
27# que armarse **bajo el montaje del store**. Y el ensamblado de la imagen vuelve a enlazar, esta vez
28# hacia `work/`, que está en OTRO disco — así que la composición se copia una vez a este lado. Las
29# dos restricciones son la misma física y ya mordieron dos veces en este repo.
30#
31# Uso:  scripts/instalador-variantes.sh [servidor|mirada|full]...   (sin argumentos: las tres)
32# Vars: KERNEL · SALIDA (def work/variantes) · STORE (def /store)
33set -eu
34ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
35STORE="${STORE:-/store}"
36SALIDA="${SALIDA:-work/variantes}"
37COMPOSICION="$STORE/.perfiles"
38LOCAL="work/.rootfs-local"
39
40# ── perfiles_de <variante> <medio> ────────────────────────────────────────────────────────────
41# ⚠ **El medio manda sobre el contenido, y `servidor` es el caso donde se nota.** El fichero único
42# carga el sistema ENTERO en memoria —es lo que le permite soltar el disco para poder formatearlo—
43# así que cada MiB del rootfs es RAM exigida en la máquina de quien instala. El USB no tiene ese
44# límite: corre desde el propio medio.
45#
46# Por eso `servidor` no es la misma cosa en los dos:
47#   · por el archivo único va `servidor-minimo` — los mismos servicios, sin la cadena de compilar
48#     (`rust` 364 MiB, `lld21` 109) ni el toolbox de `cli` (54 raíces). 1,6 GiB contra 3,2.
49#   · por USB va `servidor` entero, que es el destino de verdad.
50#
51# No es una opinión sobre qué debe llevar un servidor: es una restricción física del medio, y está
52# escrita en los dos sitios —acá y en el `descripcion` del perfil— para que nadie la lea como un
53# recorte arbitrario.
54perfiles_de() {
55    case "$1:${2:-usb}" in
56        # `base` no compone NINGÚN perfil: es el piso de arranque solo, que es lo que la variante
57        # promete. Devuelve vacío a propósito y el bucle de hidratación no da ni una vuelta.
58        base:*)       echo "" ;;
59        servidor:run) echo "base servidor-minimo" ;;
60        servidor:*)   echo "base servidor" ;;
61        mirada:*)     echo "base escritorio-mirada" ;;
62        # ⚠ **GNOME estuvo fuera medio día, por dos cosas suyas, y las dos están resueltas.**
63        #   · `accountsservice` trae `/var/lib/AccountsService/users` en 0700 ⇒ hidratar como
64        #     usuario normal no podía leerlo y el nodo no proyectaba. Se arma con privilegio, que es
65        #     lo que ya hacía falta igual por los 0500 de ModemManager.
66        #   · `usr/libexec/libgtop_server2` llega 4555 desde el tarball de upstream y el perfil no lo
67        #     declara. El aviso del auditor decía «o entra a un `[[setuid]]` con su motivo, **o
68        #     sale**» y hasta ese día sólo existía la primera mitad. Ahora `takana setuid --apply`
69        #     RETIRA el bit no declarado, rompiendo el enlace duro: el store queda intacto y se dice
70        #     cuál se quitó. Es la opción segura —quita privilegio, nunca lo da— y la coherente con
71        #     el invariante: `of_tree` no hashea ese bit, así que un artefacto que lo trae está
72        #     fuera de contrato.
73        full:*)       echo "base cli escritorio-mirada escritorio-sway escritorio-cosmic escritorio-gnome escritorio-kde servidor" ;;
74        *) echo "variante desconocida: $1 (servidor|mirada|full)" >&2; return 1 ;;
75    esac
76}
77
78# ── QUÉ MEDIOS TIENE CADA VARIANTE, Y POR QUÉ `full` NO TIENE FICHERO ÚNICO ────────────────────────────
79# El fichero único carga el instalador ENTERO en memoria —es lo que le permite soltar el disco— y
80# en RAM viven a la vez el initramfs comprimido y su contenido desempaquetado. El generador ya lo
81# comprueba y se niega por encima del 40 % de la RAM; con `full` (3,2 GiB) eso pide ~8 GiB en la
82# máquina donde se instale, o sea que el fichero único sería uno de 1,5 GiB que en la mitad de las
83# máquinas no se puede usar. **El USB no tiene ese límite**: corre desde el propio medio.
84#
85# ⚠ Y hay un defecto conocido en esa guarda: mide la RAM de la máquina que CONSTRUYE, no la de la
86# que instala. Acá da el resultado correcto por casualidad —el hub tiene 7,6 GiB, parecido a un
87# portátil— pero construir en una caja grande dejaría pasar una imagen que allá no arranca. Está
88# anotado y no barrido.
89medios_de() {
90    case "$1" in
91        full) echo "usb" ;;
92        *)    echo "run usb" ;;
93    esac
94}
95
96VARIANTES="${*:-base mirada servidor full}"
97for v in $VARIANTES; do perfiles_de "$v" usb >/dev/null || exit 2; done
98
99KERNEL="${KERNEL:-$(ls -dt "$STORE"/*-linux-metal/boot/bzImage 2>/dev/null | head -1)}"
100[ -n "${KERNEL:-}" ] && [ -r "$KERNEL" ] || { echo "no encuentro un bzImage de linux-metal (set KERNEL=)" >&2; exit 1; }
101PISO="${PISO:-$(ls -dt "$STORE"/*-product-rootfs 2>/dev/null | head -1)}"
102[ -n "${PISO:-}" ] && [ -d "$PISO" ] || { echo "no encuentro un product-rootfs (set PISO=)" >&2; exit 1; }
103echo "==> piso de arranque: $PISO"
104echo "==> kernel: $KERNEL"
105mkdir -p "$SALIDA" "$COMPOSICION" "$LOCAL"
106
107compone() {   # <variante> <medio> → deja el árbol local en $LOCAL/<variante>-<medio>
108    _cv="$1"; _cm="$2"
109    C="$COMPOSICION/$_cv-$_cm"
110    # `cp -al`: el piso entra por hardlinks, que en el mismo filesystem es gratis. Un `cp -a` de
111    # 265 MiB por variante es tiempo y disco que no hacen falta.
112    rm -rf "$C"; cp -al "$PISO" "$C"
113    chmod -R u+w "$C" 2>/dev/null || true
114    # ⚠ **Cada perfil se hidrata en SU PROPIO árbol y después se funden**, y no uno encima de otro
115    # con `--keep`. La razón es la auditoría del bit de privilegio, que corre contra las
116    # declaraciones `[[setuid]]` DEL PERFIL QUE SE ESTÁ HIDRATANDO: al poner `escritorio-mirada`
117    # encima de `base`, veía el `doas` 4755 que dejó `base` —declarado, legítimo— y lo denunciaba
118    # como «un bit que nadie declaró». La auditoría tiene razón en lo suyo; el que estaba mal era
119    # el método. Medido el 2026-09-23.
120    for p in $(perfiles_de "$_cv" "$_cm"); do
121        echo "  ── perfil $p"
122        H="$COMPOSICION/$_cv-$_cm-$p"; rm -rf "$H"
123        python3 scripts/hydrate-profile.py "$p" --into "$H" --store "$STORE" \
124            > "work/.variante-$_cv-$p.log" 2>&1 || {
125            echo "     ✗ falló la hidratación de $p — ver work/.variante-$_cv-$p.log" >&2
126            grep -A6 'SIN artefacto\|✗' "work/.variante-$_cv-$p.log" | head -14 >&2
127            exit 1; }
128        # `-n` (no pisar): si dos perfiles traen el mismo fichero es el mismo artefacto, así que
129        # gana el primero y da igual cuál sea. Pisar daría el mismo resultado y un error si el
130        # hardlink ya existe.
131        chmod -R u+w "$H" 2>/dev/null || true
132        # ⚠ **Un `-n` silencioso es una decisión escondida.** Cuando dos perfiles traen el MISMO
133        # camino gana el primero, y con un solo escritorio eso es siempre el mismo artefacto y da
134        # igual. Con cinco escritorios en la misma imagen ya no: dos de ellos pueden querer el
135        # mismo `/etc/xdg/...` o el mismo `.desktop`, y quedarse callado significa que uno pisa al
136        # otro sin que nadie lo sepa. No se resuelve acá —hace falta saber CUÁL gana y por qué—
137        # pero se CUENTA, que es lo mínimo para que exista la pregunta.
138        _antes=$(find "$C" ! -type d 2>/dev/null | wc -l)
139        _trae=$(find "$H" ! -type d 2>/dev/null | wc -l)
140        ( cd "$H" && find . -type d -exec mkdir -p "$C/{}" \; ) 2>/dev/null || true
141        ( cd "$H" && find . ! -type d -exec cp -a -l -n {} "$C/{}" \; ) 2>/dev/null || true
142        _despues=$(find "$C" ! -type d 2>/dev/null | wc -l)
143        _choques=$(( _antes + _trae - _despues ))
144        [ "$_choques" -gt 0 ] && echo "     ⚠ $_choques fichero(s) ya estaban y NO se pisaron (gana el primero)"
145        rm -rf "$H"
146        echo "     $(grep -oE '[0-9]+/[0-9]+' "work/.variante-$_cv-$p.log" | tail -1) artefactos · acumulado $(du -sm "$C" | cut -f1) MiB"
147    done
148    # ── el árbol tiene que quedar en el MISMO montaje que el stage del ensamblado ─────────────
149    # ⚠ **Algunos artefactos traen ficheros que el usuario normal no puede LEER** —ModemManager
150    # instala sus `fcc-unlock.available.d/*` en 0500, que es como vienen de upstream— así que copiar
151    # el árbol pide privilegio. No se hace `chown`: `cp -a` como root conserva dueño y modo, que es
152    # justo lo que la imagen tiene que llevar. Aparece en cuanto entra un escritorio (sway, cosmic,
153    # kde); las variantes chicas nunca lo tocaron. Medido el 2026-09-23.
154    L="$LOCAL/$_cv-$_cm"
155    rm -rf "$L" 2>/dev/null || sudo rm -rf "$L"
156    if ! cp -a "$C" "$L" 2>/dev/null; then
157        rm -rf "$L" 2>/dev/null || sudo rm -rf "$L"
158        echo "     (hay ficheros que sólo root puede leer: copiando con privilegio)"
159        sudo cp -a "$C" "$L" || { echo "     ✗ no pude copiar el árbol ni con privilegio" >&2; exit 1; }
160    fi
161    rm -rf "$C" 2>/dev/null || sudo rm -rf "$C"
162    echo "     árbol local: $(du -sm "$L" | cut -f1) MiB"
163}
164
165for v in $VARIANTES; do
166    echo
167    echo "════ $v ════════════════════════════════════════════════════════════════════════"
168
169  if echo " $(medios_de "$v") " | grep -q ' run '; then
170    echo "  ── el fichero que se baja (.sh) — perfiles: $(perfiles_de "$v" run)"
171    compone "$v" run
172    ROOTFS="$L" KERNEL="$KERNEL" SALIDA="$SALIDA/paquete-$v" ./scripts/instalador-paquete.sh \
173        > "work/.variante-$v-paquete.log" 2>&1 || {
174        echo "     ✗ falló el paquete — ver work/.variante-$v-paquete.log" >&2; tail -6 "work/.variante-$v-paquete.log" >&2; exit 1; }
175    PAQUETE="$SALIDA/paquete-$v" SALIDA="$SALIDA/takana-instalador-$v.sh" ./scripts/instalador-autoextraible.sh \
176        > "work/.variante-$v-run.log" 2>&1 || {
177        echo "     ✗ falló el autoextraíble — ver work/.variante-$v-run.log" >&2; tail -6 "work/.variante-$v-run.log" >&2; exit 1; }
178    echo "     $SALIDA/takana-instalador-$v.sh  $(( $(wc -c < "$SALIDA/takana-instalador-$v.sh") / 1048576 )) MiB"
179    rm -rf "$L"
180  else
181    echo "  ── sin fichero único: este rootfs no entra en RAM en una máquina normal (sólo USB)"
182  fi
183
184    echo "  ── la imagen USB — perfiles: $(perfiles_de "$v" usb)"
185    compone "$v" usb
186    ROOTFS="$L" KERNEL="$KERNEL" IRD_TREE="work/.arbol-$v" \
187        IMG="$SALIDA/takana-instalador-usb-$v.img" ./scripts/instalador-usb.sh \
188        > "work/.variante-$v-usb.log" 2>&1 || {
189        echo "     ✗ falló el USB — ver work/.variante-$v-usb.log" >&2; tail -6 "work/.variante-$v-usb.log" >&2; exit 1; }
190    echo "     $SALIDA/takana-instalador-usb-$v.img  $(( $(wc -c < "$SALIDA/takana-instalador-usb-$v.img") / 1048576 )) MiB"
191    rm -rf "$L"
192done
193
194echo
195echo "✓ en $SALIDA:"
196ls -l "$SALIDA"/*.sh "$SALIDA"/*.img 2>/dev/null | awk '{printf "  %-52s %s MiB\n", $9, int($5/1048576)}'