#!/bin/sh # instalador-autoextraible.sh — UN fichero que se baja y sabe hacer todo. # # ══ POR QUÉ ═══════════════════════════════════════════════════════════════════════════════════ # El paquete del instalador son cuatro cosas (el binario, el instalador en RAM, su kernel y el # censo) y hasta hoy había que copiarlas juntas, con su árbol, y saber cuál correr. Eso sirve para # quien ya sabe; no sirve para publicar. Lo que se publica tiene que ser **un fichero**: se baja, se # le da permiso de ejecución, se corre, y él se encarga. # # curl -LO https://takana-os.net/descargas/takana-instalador.sh # chmod +x takana-instalador.sh # sudo ./takana-instalador.sh # # ── CÓMO ESTÁ HECHO ─────────────────────────────────────────────────────────────────────────── # Cabecera `sh` + payload `tar.gz` pegado detrás, separado por OFFSET DE BYTES y no por número de # línea: un payload binario puede traer cualquier cosa, y `tail -n +N` sobre bytes arbitrarios es # una apuesta. El offset se calcula midiendo la cabecera ya escrita, con un hueco de ancho fijo para # que sustituirlo no cambie el tamaño de lo que se midió. # # ── LO QUE NO HACE, A PROPÓSITO ─────────────────────────────────────────────────────────────── # **No baja nada.** Todo viaja adentro: sin red, sin espejo, sin resolver un nombre. Un instalador # de sistema que necesita internet para instalar falla justo donde más caro sale — en la máquina que # todavía no tiene sistema. El precio son ~140 MiB, y se pagan una vez. # # Uso: scripts/instalador-autoextraible.sh [--paquete ] [--salida ] set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT" PAQUETE="${PAQUETE:-work/instalador-salida}" SALIDA="${SALIDA:-work/takana-instalador.sh}" while [ $# -gt 0 ]; do case "$1" in --paquete) PAQUETE="${2:?}"; shift 2 ;; --salida) SALIDA="${2:?}"; shift 2 ;; -h|--help) sed -n '2,30p' "$0"; exit 0 ;; *) echo "opción desconocida: $1" >&2; exit 2 ;; esac done for f in takana takana-instalador.cpio.gz kernel/boot/bzImage takana-install-partition.sh \ lib/herramientas-desde-store.sh; do [ -r "$PAQUETE/$f" ] || { echo "el paquete $PAQUETE no trae $f — armalo con scripts/instalador-paquete.sh" >&2; exit 1; } done . "$(dirname "$0")/lib/herramientas-desde-store.sh" GZIP=$(herramienta gzip bin/gzip gzip) || exit 1 TMP="work/.autoextraible"; rm -rf "$TMP"; mkdir -p "$TMP" # ── 1) el payload ───────────────────────────────────────────────────────────────────────────── # `tar` del host: es el único punto donde no se usa una herramienta sellada, y es a propósito — # lo que importa del tar es que lo pueda LEER cualquier Linux, no con qué se escribió. El formato # es el POSIX de siempre. echo "==> payload" tar -C "$PAQUETE" -cf "$TMP/payload.tar" . 2>/dev/null || { echo "no pude armar el tar del paquete (¿hay 'tar' en el PATH?)" >&2; exit 1; } "$GZIP" -9 -c "$TMP/payload.tar" > "$TMP/payload.tar.gz" rm -f "$TMP/payload.tar" SHA=$(sha256sum "$TMP/payload.tar.gz" | cut -d' ' -f1) MIB=$(( $(wc -c < "$TMP/payload.tar.gz") / 1048576 )) echo " $MIB MiB · sha256 $(echo "$SHA" | cut -c1-16)…" # ── 2) la cabecera ──────────────────────────────────────────────────────────────────────────── # `@@@@@@@@@@` son diez caracteres y el offset se escribe con diez dígitos: la sustitución no mueve # ni un byte de lo que ya se midió. Si alguien cambia el ancho de uno, tiene que cambiar el del otro. cat > "$TMP/cabecera" <&2; exit 1; } decir() { printf '%s\n' "\$*"; } morir() { printf '%s\n' "\$*" >&2; exit 1; } MODO=instalar; DESTINO_EXTRAER="" case "\${1:-}" in --check) MODO=check; shift ;; --survey) MODO=survey; shift ;; --extraer) MODO=extraer; DESTINO_EXTRAER="\${2:?--extraer necesita un directorio}"; shift 2 ;; --sha256) printf '%s payload\n' "\$SHA_ESPERADO"; exit 0 ;; -h|--help) sed -n '2,12p' "\$AUTO" | sed 's/^# \{0,1\}//'; exit 0 ;; esac # ── la máquina ──────────────────────────────────────────────────────────────────────────────── # El binario y el kernel son x86-64. En otra arquitectura no hay nada que intentar, y decirlo acá # es la diferencia entre un mensaje claro y un "cannot execute binary file" tres pasos después. ARQ=\$(uname -m 2>/dev/null || echo desconocida) case "\$ARQ" in x86_64|amd64) ;; *) morir "takana-instalador: esto es para x86-64 y esta máquina es \$ARQ. No hay nada acá que le sirva." ;; esac # ── dónde desempaquetar ─────────────────────────────────────────────────────────────────────── # \`/var/tmp\` ANTES que \`/tmp\`: en casi toda distro moderna \`/tmp\` es tmpfs, o sea RAM, y acá se # desempaquetan ~\$PAYLOAD_MIB MiB que después se vuelven a cargar en memoria para el salto. Hacerlo # en RAM lo cuenta dos veces, y en una máquina justa eso es un OOM en el peor momento. if [ -n "\$DESTINO_EXTRAER" ]; then DIR="\$DESTINO_EXTRAER"; mkdir -p "\$DIR" else BASE="\${TAKANA_TMP:-}" [ -n "\$BASE" ] || for c in /var/tmp /tmp; do [ -d "\$c" ] && [ -w "\$c" ] && { BASE="\$c"; break; }; done [ -n "\$BASE" ] || morir "takana-instalador: no encuentro un directorio donde escribir (probá TAKANA_TMP=/ruta)" DIR="\$BASE/takana-instalador.\$\$" mkdir -p "\$DIR" # Sin \`trap\`, un fallo a mitad deja \$PAYLOAD_MIB MiB tirados con un nombre que nadie reconoce. # Si el salto ocurre, esto no corre nunca — y es correcto: para entonces ya no hay userspace. trap 'rm -rf "\$DIR"' EXIT INT TERM fi # El espacio se mide ANTES: un \`tar\` que se queda sin disco a mitad deja un árbol incompleto y el # error que sale es del último fichero, no del disco. LIBRE=\$(df -Pk "\$DIR" 2>/dev/null | awk 'NR==2 {print int(\$4/1024)}') if [ -n "\${LIBRE:-}" ] && [ "\$LIBRE" -lt \$(( PAYLOAD_MIB + 64 )) ]; then morir "takana-instalador: hacen falta ~\$(( PAYLOAD_MIB + 64 )) MiB en \$DIR y hay \$LIBRE MiB. Probá con TAKANA_TMP=/ruta/con/espacio." fi # ── el payload ──────────────────────────────────────────────────────────────────────────────── decir "==> desempaquetando (\$PAYLOAD_MIB MiB) en \$DIR" tail -c +\$(( OFFSET + 1 )) "\$AUTO" > "\$DIR/payload.tar.gz" || morir "no pude leer el payload" # La comprobación de integridad NO se salta en silencio. Si no hay con qué hacerla, se dice: un # chequeo ausente y uno que pasó se ven igual en pantalla si nadie lo cuenta. SUMA="" for h in sha256sum "shasum -a 256"; do if command -v "\${h%% *}" >/dev/null 2>&1; then SUMA=\$(\$h "\$DIR/payload.tar.gz" 2>/dev/null | cut -d' ' -f1); break fi done if [ -z "\$SUMA" ] && command -v openssl >/dev/null 2>&1; then SUMA=\$(openssl dgst -sha256 "\$DIR/payload.tar.gz" 2>/dev/null | sed 's/.*= *//') fi if [ -z "\$SUMA" ]; then decir " ⚠ esta máquina no tiene sha256sum, shasum ni openssl: el payload NO se verificó." elif [ "\$SUMA" != "\$SHA_ESPERADO" ]; then morir "takana-instalador: el payload no coincide con su sha256. esperado \$SHA_ESPERADO leído \$SUMA La descarga se cortó, o el fichero no es el que se publicó. No sigo." else decir " ✓ payload verificado (sha256)" fi gzip -d -c "\$DIR/payload.tar.gz" | tar -x -C "\$DIR" || morir "no pude desempaquetar (¿hay gzip y tar?)" rm -f "\$DIR/payload.tar.gz" chmod +x "\$DIR/takana" 2>/dev/null || true if [ "\$MODO" = extraer ]; then trap - EXIT INT TERM decir "✓ desempaquetado en \$DIR" decir " el instalador es: sudo \$DIR/takana metal install" exit 0 fi TK="\$DIR/takana" [ -x "\$TK" ] || morir "el payload no trae un 'takana' ejecutable" # ⚠ **Sin \`exec\`, y no es un detalle de estilo: \`exec\` se lleva puesto el \`trap\`.** Estas dos # ramas eran \`exec "\$TK" metal …\` y dejaban los \$PAYLOAD_MIB MiB desempaquetados tirados en el # disco DESPUÉS DE CADA MIRADA — y \`--check\`/\`--survey\` son justamente los que alguien corre # varias veces antes de decidirse. Medido acá el 2026-09-23: 141 MiB en /tmp tras un \`--check\`. # En una máquina con la raíz chica eso no se degrada: se llena y la tira. case "\$MODO" in check) set +e; "\$TK" metal check; RC=\$?; set -e; exit "\$RC" ;; survey) set +e; "\$TK" metal survey; RC=\$?; set -e; exit "\$RC" ;; esac # ── el camino completo ──────────────────────────────────────────────────────────────────────── # check informa, survey enseña y install pregunta. Los tres por separado y en ese orden, porque es # el orden en el que una persona necesita saber las cosas: si se puede, qué hay, y recién entonces # qué se va a hacer. decir "" if ! "\$TK" metal check; then decir "" decir "Con la imagen USB se instala igual: el instalador que viaja acá adentro es el mismo." exit 1 fi decir "" "\$TK" metal survey || true decir "" # Sin \`exec\`: el \`trap\` de limpieza tiene que seguir vivo si el salto NO ocurre. "\$TK" metal install "\$@" HDREOF # `exit 0` cierra la cabecera: sin él, el shell seguiría leyendo el payload como si fuera guion. printf '%s\n' 'exit 0' >> "$TMP/cabecera" # ── 3) el offset, medido sobre la cabecera ya escrita ───────────────────────────────────────── LARGO=$(wc -c < "$TMP/cabecera") sed "s/@@@@@@@@@@/$(printf '%010d' "$LARGO")/" "$TMP/cabecera" > "$TMP/cabecera.final" LARGO2=$(wc -c < "$TMP/cabecera.final") [ "$LARGO" = "$LARGO2" ] || { echo "la sustitución del offset cambió el tamaño ($LARGO → $LARGO2)" >&2; exit 1; } mkdir -p "$(dirname "$SALIDA")" cat "$TMP/cabecera.final" "$TMP/payload.tar.gz" > "$SALIDA" chmod 0755 "$SALIDA" rm -rf "$TMP" # ── 4) el control: que se desempaquete de verdad ────────────────────────────────────────────── # Un autoextraíble que no se probó es un fichero de 140 MiB que nadie sabe si abre. Se extrae acá # mismo y se compara con el paquete de origen, fichero por fichero. echo "==> control: desempaquetar y comparar" V="work/.autoextraible-verif"; rm -rf "$V" "./$SALIDA" --extraer "$V" >/dev/null || { echo "el autoextraíble no se desempaqueta" >&2; exit 1; } FALLOS=0 for f in takana takana-instalador.cpio.gz kernel/boot/bzImage takana-install-partition.sh; do a=$(sha256sum "$PAQUETE/$f" | cut -d' ' -f1) b=$(sha256sum "$V/$f" 2>/dev/null | cut -d' ' -f1) if [ "$a" = "$b" ]; then echo " ✓ $f"; else echo " ✗ $f NO coincide" >&2; FALLOS=$((FALLOS+1)); fi done rm -rf "$V" [ "$FALLOS" = 0 ] || exit 1 echo echo "✓ $SALIDA ($(( $(wc -c < "$SALIDA") / 1048576 )) MiB)" echo " sha256: $(sha256sum "$SALIDA" | cut -d' ' -f1)" echo echo " curl -LO https://takana-os.net/descargas/takana-instalador.sh" echo " chmod +x takana-instalador.sh && sudo ./takana-instalador.sh"