- install.sh: checks Python 3.12+, installs the app into its own environment, checks Qt's system libraries, installs icons and a --background .desktop launcher, offers start at login; --uninstall. - install.bat / install_windows.ps1: checks Python (offering winget), builds with PyInstaller, installs to LOCALAPPDATA with Start Menu and Desktop shortcuts, offers start at login; -Uninstall. - .gitattributes keeps .bat/.ps1 as CRLF and .sh as LF. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
224 lines
9.3 KiB
Bash
Executable File
224 lines
9.3 KiB
Bash
Executable File
#!/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" <<EOF
|
|
[Desktop Entry]
|
|
Type=Application
|
|
Name=$APP_NAME
|
|
GenericName=MTG Arena Tracker
|
|
Comment=Deck and game tracker for Magic: The Gathering Arena
|
|
Exec="$BIN" --background
|
|
Icon=$APP_ID
|
|
Terminal=false
|
|
Categories=Game;
|
|
Keywords=magic;mtg;arena;mtga;tracker;deck;
|
|
StartupWMClass=$APP_ID
|
|
EOF
|
|
if command -v desktop-file-validate >/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/<size>/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
|