fosfeno/install.sh
hacklab e3db98b089 install.sh: no forzar nodejs/npm de apt si Node ya esta instalado
El paquete 'npm' de Debian entra en conflicto con el 'nodejs' de NodeSource, lo que hacia fallar toda la instalacion apt. Ahora, si 'node' y 'npm' ya existen, se respetan; solo se instalan desde apt cuando faltan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:06:06 +02:00

347 lines
14 KiB
Bash
Executable file

#!/usr/bin/env bash
# ===========================================================================
# FOSFENO :: instalador para Raspberry Pi OS Bookworm (Raspberry Pi 4 y 5)
#
# Uso: bash install.sh # Raspberry Pi: instala todo
# bash install.sh --laptop # portatil Linux (Debian/Ubuntu/Mint)
# bash install.sh --no-projectm # omite la compilacion de projectM
# bash install.sh --check # solo comprueba el sistema, no instala
# ===========================================================================
set -uo pipefail
DIR="$(cd "$(dirname "$0")" && pwd)"
source "$DIR/scripts/lib.sh"
MODE="install"
SKIP_PM="no"
LAPTOP="no"
for arg in "$@"; do
case "$arg" in
--no-projectm) SKIP_PM="yes" ;;
--check) MODE="check" ;;
--laptop) LAPTOP="yes" ;;
*) echo "Opcion desconocida: $arg"; exit 1 ;;
esac
done
# Versiones minimas requeridas
MIN_PYTHON="3.9"
MIN_NODE="16"
MIN_NPM="8"
MIN_CMAKE="3.21"
printf '%s\n' "$C_BLD==================================================="
printf '%s\n' " FOSFENO :: instalador ($DIR)"
printf '%s\n' "===================================================$C_RST"
# ---------------------------------------------------------------------------
# 1. Comprobacion del hardware y del sistema operativo
# ---------------------------------------------------------------------------
log_step "[1/9] Comprobando el sistema"
if [ -r /etc/os-release ]; then
. /etc/os-release
log_info "Sistema: ${PRETTY_NAME:-desconocido}"
fi
if [ "$LAPTOP" = "yes" ]; then
log_ok "Modo portatil (Linux de escritorio)"
if ! need_cmd apt-get; then
log_fail "El modo --laptop usa apt (Debian, Ubuntu o Mint)."
log_fail "Tu sistema no tiene apt; este instalador no sirve aqui."
exit 1
fi
else
MODELO="$(pi_model)"
log_info "Modelo detectado: $MODELO"
case "$MODELO" in
*"Raspberry Pi 5"*) log_ok "Raspberry Pi 5 (soportada)" ;;
*"Raspberry Pi 4"*) log_ok "Raspberry Pi 4 (soportada)" ;;
*"Raspberry Pi"*) warn "Raspberry Pi distinta de 4/5: puede funcionar pero sin garantias" ;;
*) warn "No parece una Raspberry Pi. Si es un portatil, usa: bash install.sh --laptop" ;;
esac
if [ "${VERSION_CODENAME:-}" != "bookworm" ]; then
warn "Se recomienda Raspberry Pi OS Bookworm (detectado: ${VERSION_CODENAME:-?})"
fi
fi
# ---------------------------------------------------------------------------
# Modo --check: solo verifica lo que ya esta instalado y termina
# ---------------------------------------------------------------------------
if [ "$MODE" = "check" ]; then
log_step "Comprobando herramientas ya instaladas"
require_version "Python" "python3 --version" "$MIN_PYTHON" || true
require_version "Node.js" "node --version" "$MIN_NODE" || true
require_version "npm" "npm --version" "$MIN_NPM" || true
require_version "CMake" "cmake --version" "$MIN_CMAKE" || true
if need_cmd chromium-browser || need_cmd chromium; then
log_ok "Chromium presente"
else
log_fail "Chromium no encontrado"
fi
need_cmd git && log_ok "git presente" || log_fail "git no encontrado"
need_cmd projectMSDL && log_ok "projectM (projectMSDL) presente" \
|| log_warn "projectM no instalado (opcional)"
printf '\n'
[ "$FOSFENO_WARNINGS" -eq 0 ] && log_ok "Comprobacion terminada sin avisos" \
|| log_warn "Comprobacion terminada con $FOSFENO_WARNINGS aviso(s)"
exit 0
fi
# ---------------------------------------------------------------------------
# 2. Paquetes del sistema
# ---------------------------------------------------------------------------
log_step "[2/9] Instalando dependencias del sistema (apt)"
sudo apt-get update
if sudo apt-get install -y \
python3 python3-venv python3-pip \
git cmake build-essential pkg-config \
libsdl2-dev libgles2-mesa-dev mesa-common-dev libglm-dev libpoco-dev \
pulseaudio-utils \
v4l-utils \
avahi-daemon \
xdotool unclutter; then
log_ok "Paquetes apt instalados"
else
log_fail "Fallo al instalar paquetes apt"
exit 1
fi
# Node.js y npm. Si ya estan (por ejemplo instalados desde NodeSource) no se
# tocan: el paquete 'npm' de Debian entra en conflicto con el 'nodejs' de
# NodeSource, y forzarlo rompe la instalacion entera.
if need_cmd node && need_cmd npm; then
log_ok "Node.js y npm ya estaban instalados ($(node --version 2>/dev/null))"
elif need_cmd node; then
warn "Node.js esta, pero falta npm; instalalo con el mismo metodo que node"
elif sudo apt-get install -y nodejs npm 2>/dev/null; then
log_ok "Node.js y npm instalados"
else
warn "no se pudo instalar Node.js; instalalo a mano (apt o NodeSource) y reintenta"
fi
# El navegador se llama 'chromium' en Debian/Ubuntu/Mint actuales y
# 'chromium-browser' en Raspberry Pi OS. Instalamos el que exista.
if need_cmd chromium || need_cmd chromium-browser; then
log_ok "Chromium ya estaba instalado"
elif sudo apt-get install -y chromium 2>/dev/null; then
log_ok "Chromium instalado (paquete 'chromium')"
elif sudo apt-get install -y chromium-browser 2>/dev/null; then
log_ok "Chromium instalado (paquete 'chromium-browser')"
else
warn "no se pudo instalar Chromium automaticamente; instalalo a mano"
fi
# ---------------------------------------------------------------------------
# 3. Verificacion de versiones de las herramientas
# ---------------------------------------------------------------------------
log_step "[3/9] Verificando versiones de las herramientas"
require_version "Python" "python3 --version" "$MIN_PYTHON" \
|| { log_fail "Python demasiado antiguo; aborto"; exit 1; }
require_version "Node.js" "node --version" "$MIN_NODE" \
|| warn "Node.js antiguo: 'npm install' podria fallar"
require_version "npm" "npm --version" "$MIN_NPM" || true
if [ "$SKIP_PM" = "no" ]; then
require_version "CMake" "cmake --version" "$MIN_CMAKE" \
|| { warn "CMake antiguo: projectM no se compilara"; SKIP_PM="yes"; }
fi
# ---------------------------------------------------------------------------
# 4. Entorno Python
# ---------------------------------------------------------------------------
log_step "[4/9] Creando el entorno virtual de Python"
python3 -m venv "$DIR/.venv"
"$DIR/.venv/bin/pip" install --quiet --upgrade pip
"$DIR/.venv/bin/pip" install --quiet -r "$DIR/backend/requirements.txt"
if "$DIR/.venv/bin/python3" -c "import flask, flask_socketio" 2>/dev/null; then
log_ok "Entorno Python listo (flask, flask-socketio)"
else
log_fail "Las dependencias de Python no se importan correctamente"
exit 1
fi
# ---------------------------------------------------------------------------
# 5. Librerias web (Butterchurn, Hydra, Socket.IO, CodeMirror)
# ---------------------------------------------------------------------------
log_step "[5/9] Descargando librerias de visuales (npm)"
cd "$DIR/web"
if npm install --no-audit --no-fund --loglevel=error; then
log_ok "Paquetes npm descargados"
else
log_fail "Fallo en 'npm install'"
exit 1
fi
mkdir -p "$DIR/web/lib/codemirror"
# Copia un fichero y comprueba que existe y no esta vacio
copy_lib() { # copy_lib <origen> <destino> <descripcion>
if cp "$1" "$2" 2>/dev/null && [ -s "$2" ]; then
log_ok "$3"
else
warn "no se pudo copiar: $3 ($1)"
fi
}
copy_lib node_modules/butterchurn/dist/butterchurn.min.js \
lib/butterchurn.min.js "Butterchurn"
copy_lib node_modules/butterchurn-presets/dist/base.min.js \
lib/butterchurn-presets.min.js "Presets de Butterchurn"
copy_lib node_modules/socket.io-client/dist/socket.io.min.js \
lib/socket.io.min.js "Socket.IO"
copy_lib node_modules/qrcode-generator/qrcode.js \
lib/qrcode.js "Generador de codigo QR"
copy_lib node_modules/codemirror/lib/codemirror.js \
lib/codemirror/codemirror.js "CodeMirror (editor)"
copy_lib node_modules/codemirror/lib/codemirror.css \
lib/codemirror/codemirror.css "CodeMirror (estilos)"
copy_lib node_modules/codemirror/mode/javascript/javascript.js \
lib/codemirror/javascript.js "CodeMirror (modo JavaScript)"
copy_lib node_modules/codemirror/mode/clike/clike.js \
lib/codemirror/clike.js "CodeMirror (modo GLSL)"
copy_lib node_modules/codemirror/theme/material-darker.css \
lib/codemirror/material-darker.css "CodeMirror (tema)"
# Hydra no siempre ubica el build en el mismo sitio: probamos varias rutas
if cp node_modules/hydra-synth/dist/hydra-synth.js lib/hydra-synth.js 2>/dev/null && [ -s lib/hydra-synth.js ]; then
log_ok "Hydra"
elif cp node_modules/hydra-synth/build/hydra-synth.js lib/hydra-synth.js 2>/dev/null && [ -s lib/hydra-synth.js ]; then
log_ok "Hydra (build/)"
else
warn "no se encontro el build de hydra-synth en node_modules; revisa la ruta"
fi
cd "$DIR"
# ---------------------------------------------------------------------------
# 6. projectM nativo (compilado desde fuente)
# ---------------------------------------------------------------------------
log_step "[6/9] projectM (visualizador nativo)"
if [ "$SKIP_PM" = "yes" ]; then
log_info "projectM omitido"
elif command -v projectMSDL >/dev/null 2>&1; then
log_ok "projectM ya estaba instalado ($(command -v projectMSDL))"
else
log_info "Compilando projectM desde fuente (puede tardar 10-20 min en una Pi)..."
if bash "$DIR/scripts/build-projectm.sh"; then
command -v projectMSDL >/dev/null 2>&1 \
&& log_ok "projectM compilado e instalado" \
|| warn "projectM compilo pero 'projectMSDL' no esta en el PATH"
else
warn "projectM no se pudo compilar; FOSFENO seguira con Butterchurn, Hydra y Shaders"
fi
fi
# ---------------------------------------------------------------------------
# 7. Presets de projectM y carpeta de videos
# ---------------------------------------------------------------------------
log_step "[7/9] Recursos (presets de projectM, carpeta de videos)"
mkdir -p "$DIR/data/presets-projectm" "$DIR/data/videos"
if [ -z "$(ls -A "$DIR/data/presets-projectm" 2>/dev/null)" ]; then
if git clone --depth 1 \
https://github.com/projectM-visualizer/presets-cream-of-the-crop.git \
"$DIR/data/presets-projectm" 2>/dev/null; then
N=$(find "$DIR/data/presets-projectm" -name '*.milk' | wc -l)
log_ok "Presets de projectM descargados ($N presets)"
else
warn "no se pudieron descargar los presets de projectM"
fi
else
log_ok "Presets de projectM ya presentes"
fi
log_info "Copia tus clips .mp4 en: $DIR/data/videos/"
# ---------------------------------------------------------------------------
# 8. Arranque automatico y permisos
# ---------------------------------------------------------------------------
log_step "[8/9] Arranque automatico y permisos"
chmod +x "$DIR/scripts/"*.sh "$DIR/install.sh" "$DIR/uninstall.sh" 2>/dev/null
[ -f "$DIR/fosfeno" ] && chmod +x "$DIR/fosfeno"
HOSTNAME_WANT="$(hostname)"
if [ "$LAPTOP" = "yes" ]; then
log_info "Modo portatil: sin arranque automatico ni cambios en el sistema."
log_info "FOSFENO se lanza a mano con ./fosfeno cuando quieras usarlo."
else
mkdir -p "$HOME/.config/autostart"
if sed "s#__DIR__#$DIR#g" "$DIR/scripts/fosfeno-autostart.desktop" \
> "$HOME/.config/autostart/fosfeno.desktop"; then
log_ok "Arranque automatico configurado (~/.config/autostart/fosfeno.desktop)"
fi
# Nombre de red fijo: deja el panel accesible en http://<hostname>.local/
HOSTNAME_WANT="$(python3 -c "import json;print(json.load(open('$DIR/config.json')).get('network',{}).get('hostname','fosfeno'))" 2>/dev/null || echo fosfeno)"
if [ "$(hostname)" != "$HOSTNAME_WANT" ]; then
sudo hostnamectl set-hostname "$HOSTNAME_WANT" 2>/dev/null || true
if grep -q "^127.0.1.1" /etc/hosts; then
sudo sed -i "s/^127.0.1.1.*/127.0.1.1\t$HOSTNAME_WANT/" /etc/hosts
else
echo -e "127.0.1.1\t$HOSTNAME_WANT" | sudo tee -a /etc/hosts >/dev/null
fi
log_ok "Nombre de red puesto a '$HOSTNAME_WANT' (panel en http://$HOSTNAME_WANT.local/)"
else
log_ok "Nombre de red: $HOSTNAME_WANT"
fi
if echo "$USER ALL=(ALL) NOPASSWD: /sbin/reboot, /sbin/poweroff" \
| sudo tee /etc/sudoers.d/fosfeno >/dev/null \
&& sudo chmod 440 /etc/sudoers.d/fosfeno; then
log_ok "Permisos de reinicio/apagado configurados"
fi
fi
# ---------------------------------------------------------------------------
# 9. Acceso de red
# ---------------------------------------------------------------------------
log_step "[9/9] Acceso de red"
if [ "$LAPTOP" = "yes" ]; then
log_info "Modo portatil: el panel usara el puerto 8080, no hace falta nada mas."
else
PYBIN="$(readlink -f "$DIR/.venv/bin/python3")"
if sudo setcap 'cap_net_bind_service=+ep' "$PYBIN" 2>/dev/null; then
log_ok "Puerto 80 habilitado"
else
warn "no se pudo habilitar el puerto 80; cambia 'server.port' a 8080 en config.json"
fi
fi
# ---------------------------------------------------------------------------
# Resumen final
# ---------------------------------------------------------------------------
printf '\n%s\n' "$C_BLD==================================================="
if [ "$FOSFENO_WARNINGS" -eq 0 ]; then
log_ok "FOSFENO instalado correctamente, sin avisos."
else
log_warn "FOSFENO instalado con $FOSFENO_WARNINGS aviso(s) (revisa arriba)."
fi
printf '%s\n' "===================================================$C_RST"
if [ "$LAPTOP" = "yes" ]; then
cat <<EOF
FOSFENO instalado en modo portatil.
Para arrancarlo, desde esta carpeta:
./fosfeno
Se abrira el panel de control en el navegador y una ventana aparte con
las visuales (arrastrala al proyector y pulsa F11 para pantalla completa).
Para cerrarlo todo, pulsa Ctrl+C en la terminal.
Para comprobar el sistema sin reinstalar:
bash install.sh --check
EOF
else
cat <<EOF
Siguientes pasos:
1. Activa el login automatico al escritorio:
sudo raspi-config -> System Options -> Boot / Auto Login
-> Desktop Autologin
2. Reinicia: sudo reboot
Como entrar en el panel de control:
- Al arrancar, el proyector muestra un codigo QR y la direccion.
Escanea el QR con el movil y se abre el panel. Mas facil imposible.
- Si prefieres escribirla: http://$HOSTNAME_WANT.local/
- El movil debe estar en la misma red (WiFi o cable) que la Raspberry.
Para volver a comprobar el sistema sin reinstalar:
bash install.sh --check
EOF
fi