#!/usr/bin/env bash # instalar.sh — instala o GGBOX no Linux / Steam Deck (modo Desktop). # # Para o cliente: abra o Konsole na pasta onde baixou este arquivo e rode # bash instalar.sh # (ou clique com o botão direito > "Executar no Konsole"/"Run in Konsole"). # # O que ele faz, tudo dentro da sua pasta pessoal ou do cartão SD, SEM sudo: # 1. pergunta onde instalar (padrão ~/GGBOX; oferece o cartão SD); # 2. copia o GGBOX.AppImage para lá (ou baixa, se você passar --url); # 3. cria ggbox/{roms,bios,saves,emulators-linux,prefixos} e o data/; # 4. cria o atalho no menu de aplicativos; # 5. confere o que os jogos precisam (RetroArch, Proton GE…) e só instala # alguma coisa se você disser "s"; # 6. adiciona o GGBOX à Steam como jogo não-Steam (se você quiser), para abrir # no modo Jogo. # # Opções: # --pasta DIR instala em DIR sem perguntar # --appimage ARQ usa este AppImage (padrão: GGBOX*.AppImage ao lado do script; # sem nenhum, baixa de https://linux.ggbox.com.br) # --url URL baixa o AppImage desta URL # --bootstrap ARQ ggbox_bootstrap.json (se o AppImage não trouxer embutido) # --sim responde "sim" para tudo (instala dependências e adiciona à Steam) # --sem-steam não oferece adicionar à Steam # --sem-dependencias não confere/instala dependências # # Nunca apaga data/, roms/, saves/ nem prefixos/ de uma instalação existente: # reinstalar por cima é seguro. set -euo pipefail # URL padrão do AppImage: a página linux.ggbox.com.br (VPS, linux/site/). Usada # quando não há AppImage ao lado do script — é o caso do # bash <(curl -fsSL https://linux.ggbox.com.br/instalar.sh) # (com `curl | bash` o read das perguntas comeria o próprio script; o <(…) # deixa o teclado no stdin.) URL_PADRAO="https://linux.ggbox.com.br/GGBOX.AppImage" PASTA="" APPIMAGE_ORIGEM="" URL="${GGBOX_APPIMAGE_URL:-}" BOOTSTRAP="" SIM=0 COM_STEAM=1 COM_DEPS=1 AQUI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONF_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/ggbox" APPS_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications" ICONES_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/256x256/apps" while [ $# -gt 0 ]; do case "$1" in --pasta) PASTA="${2:?--pasta precisa de um caminho}"; shift 2 ;; --appimage) APPIMAGE_ORIGEM="${2:?--appimage precisa de um arquivo}"; shift 2 ;; --url) URL="${2:?--url precisa de um endereço}"; shift 2 ;; --bootstrap) BOOTSTRAP="${2:?--bootstrap precisa de um arquivo}"; shift 2 ;; --sim|-y) SIM=1; shift ;; --sem-steam) COM_STEAM=0; shift ;; --sem-dependencias) COM_DEPS=0; shift ;; -h|--help) sed -n '2,32p' "$0"; exit 0 ;; *) echo "opção desconhecida: $1 (use --help)" >&2; exit 1 ;; esac done # Duplo clique no Dolphin roda sem terminal: as perguntas iriam para o vazio e o # script ficaria parado para sempre. Reabre dentro do Konsole. # (-f "$0": rodando por `curl ... | bash` não há arquivo para reabrir.) if [ ! -t 0 ] && [ -z "${GGBOX_NO_TERMINAL:-}" ] && [ -f "$0" ]; then export GGBOX_NO_TERMINAL=1 if command -v konsole >/dev/null 2>&1; then exec konsole --hold -e bash "$0" "$@" elif command -v x-terminal-emulator >/dev/null 2>&1; then exec x-terminal-emulator -e bash "$0" "$@" fi fi # ── Utilidades ───────────────────────────────────────────────── titulo() { printf '\n\033[1m== %s\033[0m\n' "$*"; } info() { printf ' %s\n' "$*"; } ok() { printf ' \033[32mOK\033[0m %s\n' "$*"; } falta() { printf ' \033[33m--\033[0m %s\n' "$*"; } erro() { printf '\n\033[31mERRO:\033[0m %s\n' "$*" >&2; exit 1; } perguntar() { # perguntar "texto" [padrão s|n] → 0 se sim local texto="$1" padrao="${2:-n}" resp if [ "$SIM" = "1" ]; then return 0; fi local dica="[s/N]"; [ "$padrao" = "s" ] && dica="[S/n]" read -r -p " $texto $dica " resp || resp="" resp="${resp:-$padrao}" case "$resp" in s|S|sim|Sim|SIM|y|Y) return 0 ;; *) return 1 ;; esac } # Gerenciador de pacotes da distro, para dar o comando EXATO quando faltar algo # que só o sistema instala (FUSE, Flatpak, SDL2). No SteamOS fica vazio: a raiz # é somente-leitura e o deck vem sem senha de sudo; lá tudo já vem de fábrica. GERENCIADOR="" for g in apt-get dnf pacman zypper; do command -v "$g" >/dev/null 2>&1 && { GERENCIADOR="$g"; break; } done comando_instalar() { # comando_instalar case "$GERENCIADOR" in apt-get) echo "sudo apt install -y $1" ;; dnf) echo "sudo dnf install -y $2" ;; pacman) echo "sudo pacman -S --needed $3" ;; zypper) echo "sudo zypper install -y $4" ;; *) echo "" ;; esac } tem_biblioteca() { # tem_biblioteca — no cache do ldconfig (que no Debian fica em # /sbin, fora do PATH do usuário) ou nas pastas de sempre. local lc for lc in ldconfig /sbin/ldconfig /usr/sbin/ldconfig; do if command -v "$lc" >/dev/null 2>&1 || [ -x "$lc" ]; then "$lc" -p 2>/dev/null | grep -q "$1" && return 0 break fi done local d for d in /usr/lib/x86_64-linux-gnu /lib/x86_64-linux-gnu /usr/lib64 /usr/lib /lib64 /lib; do [ -e "$d/$1" ] && return 0 done return 1 } livre_gb() { df -Pk "$1" 2>/dev/null | awk 'NR==2 {printf "%.0f", $4/1024/1024}'; } tipo_fs() { # stat -f é do coreutils (sempre presente); findmnt seria melhor mas nem # toda distro traz. local d="$1" while [ ! -e "$d" ]; do d="$(dirname "$d")"; done stat -f -c %T "$d" 2>/dev/null || echo desconhecido } [ "$(id -u)" != "0" ] || erro "não rode como root/sudo. O GGBOX instala na sua pasta pessoal; como root ele ficaria com dono errado e a Steam não enxergaria." [ "$(uname -m)" = "x86_64" ] || erro "o GGBOX para Linux é só x86_64 (o Steam Deck é)." titulo "GGBOX para Linux / Steam Deck" if [ -f /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release info "sistema: ${PRETTY_NAME:-Linux}" fi E_STEAMOS=0 [ "${ID:-}" = "steamos" ] && E_STEAMOS=1 # ── 1. Pasta ────────────────────────────────────────────────── escolher_pasta() { local opcoes=("$HOME/GGBOX") d # Cartão SD: o SteamOS monta em /run/media// (versões novas) # ou /run/media/ (antigas). Lista o que estiver montado. for d in /run/media/"$USER"/* /run/media/*; do [ -d "$d" ] && [ -w "$d" ] || continue [ "$d" = "/run/media/$USER" ] && continue opcoes+=("$d/GGBOX") done titulo "Onde instalar?" local i=1 o for o in "${opcoes[@]}"; do info "$i) $o (livre: $(livre_gb "$(dirname "$o")") GB, $(tipo_fs "$(dirname "$o")"))" i=$((i + 1)) done info "$i) outra pasta" if [ "$SIM" = "1" ]; then PASTA="${opcoes[0]}"; return; fi local esc read -r -p " Escolha [1]: " esc || esc="" esc="${esc:-1}" if [ "$esc" = "$i" ]; then read -r -p " Caminho completo: " PASTA elif [ "$esc" -ge 1 ] 2>/dev/null && [ "$esc" -lt "$i" ]; then PASTA="${opcoes[$((esc - 1))]}" else erro "opção inválida: $esc" fi } [ -n "$PASTA" ] || escolher_pasta [ -n "$PASTA" ] || erro "nenhuma pasta escolhida" PASTA="${PASTA/#\~/$HOME}" mkdir -p "$PASTA" || erro "não consegui criar $PASTA" PASTA="$(cd "$PASTA" && pwd)" [ -w "$PASTA" ] || erro "sem permissão de escrita em $PASTA" # Prefixo do Proton/Wine é cheio de link simbólico, e o AppImage precisa do # bit de execução: em FAT/exFAT/NTFS nenhum dos dois funciona. O cartão # formatado pela própria Steam é ext4 e passa. FS="$(tipo_fs "$PASTA")" case "$FS" in msdos|vfat|fat|exfat|fuseblk|ntfs|ntfs3) info "AVISO: $PASTA está em $FS. Jogos de PC (Proton) não funcionam aí e o AppImage pode não abrir." info " Formate o cartão pela Steam (Configurações > Armazenamento) ou escolha a pasta pessoal." perguntar "Continuar mesmo assim?" n || exit 1 ;; esac ok "pasta: $PASTA ($FS, $(livre_gb "$PASTA") GB livres)" # ── 2. AppImage ─────────────────────────────────────────────── titulo "Programa" DESTINO_APP="$PASTA/GGBOX.AppImage" if [ -z "$APPIMAGE_ORIGEM" ] && [ -z "$URL" ]; then # O mais novo ao lado do script (o CI entrega os dois juntos). APPIMAGE_ORIGEM="$(ls -t "$AQUI"/GGBOX*.AppImage 2>/dev/null | grep -v "^$DESTINO_APP\$" | head -n1 || true)" # Nada ao lado (rodando por `bash <(curl …)`): baixa o da página. [ -n "$APPIMAGE_ORIGEM" ] || URL="$URL_PADRAO" fi if [ -n "$APPIMAGE_ORIGEM" ]; then [ -f "$APPIMAGE_ORIGEM" ] || erro "não achei $APPIMAGE_ORIGEM" if [ "$(readlink -f "$APPIMAGE_ORIGEM")" != "$(readlink -f "$DESTINO_APP")" ]; then cp -f "$APPIMAGE_ORIGEM" "$DESTINO_APP.novo" mv -f "$DESTINO_APP.novo" "$DESTINO_APP" fi ok "AppImage copiado de $APPIMAGE_ORIGEM" elif [ -n "$URL" ]; then command -v curl >/dev/null 2>&1 || erro "precisa do curl para baixar" info "baixando $URL" curl -fL --retry 3 -o "$DESTINO_APP.novo" "$URL" || erro "falhou o download" mv -f "$DESTINO_APP.novo" "$DESTINO_APP" ok "AppImage baixado" elif [ -f "$DESTINO_APP" ]; then ok "usando o AppImage que já está em $DESTINO_APP" else erro "não achei o GGBOX*.AppImage. Coloque-o na mesma pasta deste script ou use --url." fi chmod +x "$DESTINO_APP" # Sem FUSE o AppImage não monta e nem abre: é o tropeço nº 1 de AppImage no # Ubuntu (22.04 em diante não traz o FUSE que ele usa) e em distro mínima. Em vez # de deixar o atalho falhar toda vez, extraímos o AppImage UMA vez para dentro da # pasta e o atalho abre o AppRun extraído — mesmo programa, sem montar nada. # O --appimage-extract é do próprio runtime e não precisa de FUSE. ABRIR="$DESTINO_APP" EXTRAIDO="$PASTA/.ggbox_appimage" if "$DESTINO_APP" --versao >/dev/null 2>&1; then rm -rf "$EXTRAIDO" # FUSE apareceu depois: volta a abrir o AppImage ok "AppImage abre direto (FUSE presente)" else info "o AppImage não montou (falta o FUSE nesta máquina); extraindo para $EXTRAIDO" rm -rf "$EXTRAIDO" "$PASTA/squashfs-root" ( cd "$PASTA" && "$DESTINO_APP" --appimage-extract >/dev/null 2>&1 ) || erro "não consegui extrair o AppImage. Instale o FUSE: $(comando_instalar fuse3 fuse fuse3 fuse3)" mv "$PASTA/squashfs-root" "$EXTRAIDO" ABRIR="$EXTRAIDO/AppRun" ok "programa extraído (abre igual, só não se atualiza sozinho: rode este script de novo com o AppImage novo)" CMD_FUSE="$(comando_instalar fuse3 fuse fuse3 fuse3)" [ -n "$CMD_FUSE" ] && info "Opcional, para o AppImage abrir direto: $CMD_FUSE" fi # ── 3. Estrutura e configuração ─────────────────────────────── titulo "Estrutura" mkdir -p "$PASTA/data" "$PASTA/ggbox/roms" "$PASTA/ggbox/bios" "$PASTA/ggbox/saves" \ "$PASTA/ggbox/emulators-linux" "$PASTA/ggbox/prefixos" [ -f "$PASTA/.ggbox_raiz" ] || echo "GGBOX" > "$PASTA/.ggbox_raiz" mkdir -p "$CONF_DIR" echo "$PASTA" > "$CONF_DIR/raiz" # AppRun usa se o AppImage for movido ok "ggbox/{roms,bios,saves,emulators-linux,prefixos} e data/" # Bootstrap: o do cliente nunca é sobrescrito. if [ ! -f "$PASTA/data/ggbox_bootstrap.json" ]; then [ -z "$BOOTSTRAP" ] && [ -f "$AQUI/ggbox_bootstrap.json" ] && BOOTSTRAP="$AQUI/ggbox_bootstrap.json" if [ -n "$BOOTSTRAP" ]; then cp "$BOOTSTRAP" "$PASTA/data/ggbox_bootstrap.json" ok "configuração copiada de $BOOTSTRAP" fi fi # O AppRun copia o programa para a pasta e diz se faltou o bootstrap (saída 3). # GGBOX_RAIZ: o AppRun extraído não tem $APPIMAGE para achar a pasta sozinho. set +e GGBOX_RAIZ="$PASTA" "$ABRIR" --sincronizar RC=$? set -e case "$RC" in 0) ok "programa instalado em $PASTA ($(GGBOX_RAIZ="$PASTA" "$ABRIR" --versao 2>/dev/null | head -n1))" ;; 3) falta "falta o data/ggbox_bootstrap.json (configuração do servidor)." info "Ele vem com a sua compra. Quando tiver o arquivo, rode:" info " bash instalar.sh --pasta \"$PASTA\" --bootstrap /caminho/ggbox_bootstrap.json" ;; *) erro "o AppImage não abriu (código $RC). Veja se a pasta permite executar programas." ;; esac # ── 4. Atalho no menu ───────────────────────────────────────── titulo "Atalho" mkdir -p "$APPS_DIR" "$ICONES_DIR" ICONE="ggbox" if [ -f "$PASTA/ggbox.png" ]; then cp -f "$PASTA/ggbox.png" "$ICONES_DIR/ggbox.png" else ICONE="applications-games" fi DESKTOP="$APPS_DIR/ggbox.desktop" cat > "$DESKTOP" </dev/null 2>&1 && update-desktop-database "$APPS_DIR" >/dev/null 2>&1 || true ok "GGBOX no menu de aplicativos (Jogos)" # ── 5. Dependências (espaço do usuário) ─────────────────────── FLATPAKS_RECOMENDADOS=( # id | para quê "org.libretro.RetroArch|RetroArch: consoles antigos (NES, SNES, Mega, PS1, arcade…)" "net.davidotek.pupgui2|ProtonUp-Qt: instala o Proton GE (jogos de PC, PS4/Xbox One de PC)" ) flatpak_instalado() { flatpak info "$1" >/dev/null 2>&1; } if [ "$COM_DEPS" = "1" ]; then titulo "Dependências" info "Os emuladores de cada console o GGBOX baixa sozinho na primeira vez (ggbox/emulators-linux)." info "Lista completa por console: EMULADORES.md (vem com o código, em launcher/ggbox_core/plataforma/linux/)." if command -v flatpak >/dev/null 2>&1; then for linha in "${FLATPAKS_RECOMENDADOS[@]}"; do id="${linha%%|*}"; desc="${linha#*|}" if flatpak_instalado "$id"; then ok "$desc" else falta "$desc" if perguntar "Instalar $id agora (Flathub, só para o seu usuário)?" n; then # --user: não pede senha e não mexe no sistema (o SteamOS é # somente-leitura e o deck vem sem senha de sudo). flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo # Sem terminal (assistente gráfico): sem barra de progresso, que vira lixo no log QUIETO=(); [ -t 1 ] || QUIETO=(--noninteractive) info "baixando $id do Flathub (pode levar alguns minutos)" flatpak install --user -y ${QUIETO[@]+"${QUIETO[@]}"} flathub "$id" && ok "$id instalado" || falta "não consegui instalar $id" fi fi done else # Ubuntu vem com Snap, não Flatpak. Sem Flatpak o GGBOX ainda acha o # emulador instalado pela distro (procura no PATH) ou o que ele mesmo # baixa em ggbox/emulators-linux — então é recomendado, não obrigatório. falta "flatpak não encontrado (é por ele que vêm o RetroArch e o ProtonUp-Qt recomendados)." CMD_FLATPAK="$(comando_instalar flatpak flatpak flatpak flatpak)" if [ -n "$CMD_FLATPAK" ] && [ "$E_STEAMOS" = "0" ]; then info "Para instalar: $CMD_FLATPAK (pede a sua senha; depois rode este script de novo)" # Sem terminal (--sim por script/CI) o sudo fica esperando uma senha # que ninguém vai digitar: só roda se der para perguntar ou se o # sudo não pedir senha. if [ ! -t 0 ] && ! sudo -n true 2>/dev/null; then info "(sem terminal para pedir a senha: rode o comando acima você mesmo)" elif perguntar "Rodar esse comando agora?" n; then # shellcheck disable=SC2086 — o comando é montado por nós, palavra a palavra if $CMD_FLATPAK; then ok "flatpak instalado. Rode bash instalar.sh de novo para instalar o RetroArch e o ProtonUp-Qt." info "(em algumas distros o Flatpak só aparece no menu depois de sair e entrar na sessão)" else falta "não deu certo; instale o flatpak pela loja de aplicativos da sua distro" fi fi fi CMD_RA="$(comando_instalar retroarch retroarch retroarch retroarch)" [ -n "$CMD_RA" ] && info "Sem Flatpak, o RetroArch também serve pela distro: $CMD_RA" fi # Proton GE: o ProtonUp-Qt grava em compatibilitytools.d da Steam (nativa # ou Flatpak). Basta um GE-Proton* para os jogos de PC. GE="" for d in "$HOME/.steam/root/compatibilitytools.d" "$HOME/.local/share/Steam/compatibilitytools.d" \ "$HOME/.var/app/com.valvesoftware.Steam/data/Steam/compatibilitytools.d"; do GE="$(ls -d "$d"/GE-Proton* 2>/dev/null | sort -V | tail -n1 || true)" [ -n "$GE" ] && break done if [ -n "$GE" ]; then ok "Proton GE: $(basename "$GE")" else falta "Proton GE: nenhum. Para jogos de PC abra o ProtonUp-Qt > Add version > GE-Proton." fi if command -v umu-run >/dev/null 2>&1 || [ -x "$HOME/.local/bin/umu-run" ]; then ok "umu-run (lançador do Proton fora da Steam)" else falta "umu-run: o GGBOX usa o Proton direto/Wine se não houver. Opcional." fi # SDL2: leitura de controle do Launcher. O SteamOS tem; distro mínima pode não ter. if tem_biblioteca libSDL2-2.0.so.0; then ok "SDL2 (controles)" else CMD_SDL="$(comando_instalar libsdl2-2.0-0 SDL2 sdl2 libSDL2-2_0-0)" falta "SDL2 não encontrada: o controle pode não navegar no Launcher.${CMD_SDL:+ Instale com: $CMD_SDL}" fi # Áudio do RetroArch: o AppImage oficial liga na libasound e na libjack e # NÃO abre sem elas (código 127; Ubuntu do WSL, 10/10/2026). A libasound # quase toda distro de desktop traz; a libjack o GGBOX leva uma cópia # junto do RetroArch, então aqui ela é só aviso. if tem_biblioteca libasound.so.2; then ok "ALSA (som dos emuladores)" else CMD_ALSA="$(comando_instalar libasound2 alsa-lib alsa-lib libasound2)" falta "libasound (ALSA) não encontrada: o RetroArch não abre sem ela.${CMD_ALSA:+ Instale com: $CMD_ALSA}" fi if tem_biblioteca libjack.so.0; then ok "JACK (som)" else CMD_JACK="$(comando_instalar libjack-jackd2-0 jack-audio-connection-kit pipewire-jack libjack-2_0-0)" info "libjack não encontrada: o GGBOX já leva uma cópia junto do RetroArch. Opcional.${CMD_JACK:+ ($CMD_JACK)}" fi fi # ── 6. Steam ────────────────────────────────────────────────── if [ "$COM_STEAM" = "1" ]; then titulo "Steam (modo Jogo)" if command -v steamos-add-to-steam >/dev/null 2>&1; then # É o mesmo comando do "Adicionar à Steam" do menu do KDE no SteamOS. # Recebe o .desktop, então a Steam pega nome e ícone certos. if perguntar "Adicionar o GGBOX à Steam como jogo não-Steam?" s; then if pgrep -x steam >/dev/null 2>&1; then steamos-add-to-steam "$DESKTOP" && ok "adicionado; aparece em Biblioteca > Não-Steam" \ || falta "a Steam não aceitou; faça pelo passo a passo abaixo" else falta "a Steam não está aberta. Abra a Steam e rode: steamos-add-to-steam \"$DESKTOP\"" fi fi else info "Para jogar no modo Jogo, adicione à Steam (uma vez):" info " 1. Abra a Steam no modo Desktop." info " 2. Jogos > Adicionar um jogo não-Steam à minha biblioteca…" info " 3. Marque GGBOX (ou Procurar… > $ABRIR) > Adicionar selecionados." fi info "Dica: na Steam, em Propriedades do GGBOX, o layout de controle 'Gamepad' navega o Launcher." fi titulo "Pronto" info "Abrir: menu de aplicativos > Jogos > GGBOX, ou $ABRIR" if [ -f "${BASH_SOURCE[0]}" ]; then info "Reinstalar/atualizar: rode este script de novo com o AppImage novo ao lado (seus jogos ficam)." else # Veio por bash <(curl ...): o script não existe em disco info "Atualizar: baixe o GGBOX.AppImage novo em linux.ggbox.com.br e abra, ou rode o mesmo comando de novo (seus jogos ficam)." fi