#!/usr/bin/env bash # MTGA Assistant installer for Linux. # # ./install.sh install or update (run it again after pulling a newer version) # ./install.sh --yes same, answering "yes" to every question # ./install.sh --uninstall remove the app, launcher and icons (history and settings are kept) # # It installs the app into its own Python environment (no root needed), adds a launcher # (.desktop file) and icons, optionally starts it at login, and launches it. set -euo pipefail APP_ID="mtga-assistant" APP_NAME="MTGA Assistant" MIN_PY_MINOR=12 # Python 3.12+ PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" APP_DIR="$DATA_HOME/$APP_ID/app" # the app's Python environment BIN="$APP_DIR/bin/$APP_ID" DESKTOP_FILE="$DATA_HOME/applications/$APP_ID.desktop" ICON_THEME_DIR="$DATA_HOME/icons/hicolor" ICON_SIZES=(32 48 64 128 256) AUTOSTART_FILE="$CONFIG_HOME/autostart/$APP_ID.desktop" ASSUME_YES=0 UNINSTALL=0 for arg in "$@"; do case "$arg" in -y|--yes) ASSUME_YES=1 ;; --uninstall) UNINSTALL=1 ;; -h|--help) sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "Unknown option: $arg (see --help)" >&2; exit 2 ;; esac done if [[ -t 1 ]]; then BOLD=$'\e[1m'; CYAN=$'\e[36m'; GREEN=$'\e[32m'; RED=$'\e[31m'; RESET=$'\e[0m' else BOLD=""; CYAN=""; GREEN=""; RED=""; RESET=""; fi step() { printf '\n%s==> %s%s\n' "$CYAN$BOLD" "$1" "$RESET"; } info() { printf ' %s\n' "$1"; } fail() { printf '\n%sERROR:%s %s\n' "$RED$BOLD" "$RESET" "$1" >&2; exit 1; } ask() { # ask "Question?" [default y|n] -> exit status 0 for yes local default="${2:-y}" hint answer [[ $default == y ]] && hint="[Y/n]" || hint="[y/N]" if (( ASSUME_YES )); then return 0; fi if [[ ! -t 0 ]]; then [[ $default == y ]]; return; fi read -r -p " $1 $hint " answer || true answer="${answer:-$default}" [[ ${answer,,} == y* ]] } app_running() { pgrep -u "$(id -u)" -f "(bin/$APP_ID|-m mtga_assistant)( |$)" >/dev/null 2>&1; } refresh_desktop_caches() { command -v update-desktop-database >/dev/null && update-desktop-database -q "$DATA_HOME/applications" || true command -v gtk-update-icon-cache >/dev/null && gtk-update-icon-cache -q -t "$ICON_THEME_DIR" 2>/dev/null || true } # --- uninstall ------------------------------------------------------------------------------------ if (( UNINSTALL )); then step "Uninstalling $APP_NAME" if app_running && ask "$APP_NAME is running. Quit it?"; then pkill -u "$(id -u)" -f "(bin/$APP_ID|-m mtga_assistant)( |$)" || true fi rm -rf "$APP_DIR" rm -f "$DESKTOP_FILE" "$AUTOSTART_FILE" for size in "${ICON_SIZES[@]}"; do rm -f "$ICON_THEME_DIR/${size}x${size}/apps/$APP_ID.png"; done refresh_desktop_caches info "Removed the app, its launcher, icons and login item." info "Your game history and settings are still in $DATA_HOME/$APP_ID and $CONFIG_HOME/$APP_ID;" info "delete those folders too to remove everything." exit 0 fi printf '%s%s installer%s\n' "$GREEN$BOLD" "$APP_NAME" "$RESET" info "Project folder: $PROJECT_DIR" # --- 1. Python -------------------------------------------------------------------------------------- python_ok() { # python_ok CMD -> prints version and succeeds if CMD is Python >= 3.MIN_PY_MINOR "$1" -c "import sys; assert sys.version_info >= (3, $MIN_PY_MINOR); print('%d.%d' % sys.version_info[:2])" 2>/dev/null } find_python() { local cmd for cmd in python3 python3.14 python3.13 python3.12 python; do if command -v "$cmd" >/dev/null && python_ok "$cmd" >/dev/null; then command -v "$cmd"; return 0 fi done return 1 } distro_install_hint() { # prints a package-manager command for $1 (a space-separated package list per family) local id_like="" if [[ -r /etc/os-release ]]; then # shellcheck disable=SC1091 id_like="$(. /etc/os-release; echo "${ID:-} ${ID_LIKE:-}")" fi case " $id_like " in *" arch "*) echo "sudo pacman -S --needed $1" ;; *" debian "*|*" ubuntu "*) echo "sudo apt install $2" ;; *" fedora "*|*" rhel "*) echo "sudo dnf install $3" ;; *" suse "*|*" opensuse "*) echo "sudo zypper install $4" ;; *) echo "" ;; esac } step "Checking for Python 3.$MIN_PY_MINOR or newer" if ! PYTHON="$(find_python)"; then cmd="$(distro_install_hint "python" "python3 python3-venv" "python3" "python3")" info "Python 3.$MIN_PY_MINOR+ was not found." if [[ -n $cmd ]] && ask "Install it now with: $cmd ?"; then $cmd PYTHON="$(find_python)" || fail "Python still isn't available after installing." else fail "Install Python 3.$MIN_PY_MINOR or newer${cmd:+ (e.g. $cmd)}, then run this script again." fi fi info "Using Python $(python_ok "$PYTHON") ($PYTHON)" # --- 2. App environment and dependencies ------------------------------------------------------------- step "Installing $APP_NAME and its dependencies (first run downloads ~100 MB)" if [[ ! -x $APP_DIR/bin/python ]]; then mkdir -p "$(dirname "$APP_DIR")" if ! "$PYTHON" -m venv "$APP_DIR" 2>/dev/null; then rm -rf "$APP_DIR" cmd="$(distro_install_hint "python" "python3-venv" "python3" "python3")" fail "Couldn't create a Python environment (the 'venv' module is missing).${cmd:+ Install it with: $cmd}" fi fi "$APP_DIR/bin/python" -m pip install --disable-pip-version-check --quiet --upgrade pip "$APP_DIR/bin/python" -m pip install --disable-pip-version-check --quiet --upgrade "$PROJECT_DIR" \ || fail "Installing the app failed (see the messages above)." # The version number doesn't change between updates, so make sure the new code replaces the old. "$APP_DIR/bin/python" -m pip install --disable-pip-version-check --quiet --force-reinstall --no-deps "$PROJECT_DIR" \ || fail "Updating the app failed (see the messages above)." info "Installed into $APP_DIR" # --- 3. Qt system libraries --------------------------------------------------------------------------- step "Checking that Qt can start" if ! qt_error="$(QT_QPA_PLATFORM=offscreen "$APP_DIR/bin/python" -c \ 'from PySide6.QtWidgets import QApplication; QApplication([])' 2>&1)"; then info "Qt failed to load:" printf '%s\n' "$qt_error" | tail -n 3 | sed 's/^/ /' cmd="$(distro_install_hint "libxkbcommon libglvnd fontconfig wayland xcb-util-cursor" \ "libegl1 libgl1 libxkbcommon0 libfontconfig1 libwayland-client0 libxcb-cursor0" \ "libglvnd-egl libxkbcommon fontconfig libwayland-client xcb-util-cursor" \ "libEGL1 libxkbcommon0 fontconfig libwayland-client0 libxcb-cursor0")" fail "Some system libraries Qt needs are missing.${cmd:+ Try: $cmd}" fi info "Qt loads fine." # --- 4. Icons and launcher ------------------------------------------------------------------------------ step "Installing icons and the launcher" QT_QPA_PLATFORM=offscreen "$APP_DIR/bin/python" - "$PROJECT_DIR/images/mtga-assistant-icon.png" \ "$ICON_THEME_DIR" "$APP_ID" "${ICON_SIZES[@]}" <<'PY' import sys from pathlib import Path from PySide6.QtCore import Qt from PySide6.QtGui import QGuiApplication, QImage src, theme_dir, app_id, *sizes = sys.argv[1:] app = QGuiApplication([]) img = QImage(src) if img.isNull(): sys.exit(f"can't read {src}") for size in map(int, sizes): out = Path(theme_dir) / f"{size}x{size}" / "apps" / f"{app_id}.png" out.parent.mkdir(parents=True, exist_ok=True) img.scaled(size, size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation).save(str(out)) PY mkdir -p "$(dirname "$DESKTOP_FILE")" cat > "$DESKTOP_FILE" </dev/null && ! desktop-file-validate "$DESKTOP_FILE"; then fail "The generated launcher failed validation: $DESKTOP_FILE" fi refresh_desktop_caches info "Launcher: $DESKTOP_FILE" info "Icons: $ICON_THEME_DIR//apps/$APP_ID.png" # --- 5. Start at login, launch --------------------------------------------------------------------------- step "Start at login" info "Running in the background lets it record every game and pop up when MTGA starts." if ask "Start $APP_NAME hidden when you log in?"; then "$APP_DIR/bin/python" -c "from mtga_assistant.background import set_autostart; set_autostart(True)" info "Added $AUTOSTART_FILE (change it later in the app: menu -> Start at login)." else "$APP_DIR/bin/python" -c "from mtga_assistant.background import set_autostart; set_autostart(False)" info "Skipped." fi printf '\n%s%s is installed.%s\n' "$GREEN$BOLD" "$APP_NAME" "$RESET" info "Remember to enable MTGA's detailed logs: Options -> Account -> Detailed Logs (Plugin Support)." info "To update, run ./install.sh again; to remove, ./install.sh --uninstall" if app_running; then info "$APP_NAME is already running; restart it (tray icon -> Quit, then start it again) to use this version." elif ask "Start $APP_NAME now?"; then nohup "$BIN" >/dev/null 2>&1 & disown || true fi