#!/usr/bin/env bash set -euo pipefail SOURCE_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" VERSION="${PTLINUX_VERSION:-0.5.8}" if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] then printf 'Invalid launcher version: %s (expected X.Y.Z)\n' "$VERSION" >&2 exit 2 fi DATA_HOME="${XDG_DATA_HOME:-${HOME}/.local/share}" BIN_HOME="${HOME}/.local/bin" APPLICATION_HOME="${DATA_HOME}/applications" PIXMAP_HOME="${DATA_HOME}/pixmaps" ICON_PATH="${PIXMAP_HOME}/eu.pristontale.PTLinux.png" DEFAULT_INSTALL_DIRECTORY="${HOME}/Games/PristontaleEU" LEGACY_INSTALL_DIRECTORY="${DATA_HOME}/pristontale-eu" INSTALL_DIRECTORY_EXPLICIT=0 if [[ -n "${PTLINUX_INSTALL_DIR:-}" ]] then INSTALL_DIRECTORY="$PTLINUX_INSTALL_DIR" INSTALL_DIRECTORY_EXPLICIT=1 else INSTALL_DIRECTORY="$DEFAULT_INSTALL_DIRECTORY" fi PRESERVED_LEGACY_INSTALL=0 PYTHON_BIN="${PTLINUX_PYTHON:-python3}" DRY_RUN=0 RUN_DOCTOR=1 INSTALL_DEPENDENCIES=1 usage() { printf '%s\n' \ "Usage: ./install.sh [--install-dir PATH] [--dry-run] [--skip-deps] [--skip-doctor]" \ "" \ " --install-dir PATH Store the app, managed game, and runtime data at PATH." \ " Default: ~/Games/PristontaleEU" } while (( $# > 0 )) do case "$1" in --install-dir) if (( $# < 2 )) then printf '%s\n' "--install-dir requires a path." >&2 exit 2 fi INSTALL_DIRECTORY="$2" INSTALL_DIRECTORY_EXPLICIT=1 shift 2 ;; --install-dir=*) INSTALL_DIRECTORY="${1#*=}" INSTALL_DIRECTORY_EXPLICIT=1 shift ;; --dry-run) DRY_RUN=1 shift ;; --skip-doctor) RUN_DOCTOR=0 shift ;; --skip-deps) INSTALL_DEPENDENCIES=0 shift ;; -h|--help) usage exit 0 ;; *) printf 'Unknown option: %s\n' "$1" >&2 usage >&2 exit 2 ;; esac done if (( INSTALL_DIRECTORY_EXPLICIT == 0 )) \ && [[ ! -d "${DEFAULT_INSTALL_DIRECTORY}/app/ptlinux" \ && ! -d "${DEFAULT_INSTALL_DIRECTORY}/app/current/ptlinux" ]] \ && [[ -d "${LEGACY_INSTALL_DIRECTORY}/app/ptlinux" \ || -d "${LEGACY_INSTALL_DIRECTORY}/app/current/ptlinux" \ || -f "${LEGACY_INSTALL_DIRECTORY}/launcher-prefix/drive_c/Program Files (x86)/Pristontale EU/Game.exe" ]] then INSTALL_DIRECTORY="$LEGACY_INSTALL_DIRECTORY" PRESERVED_LEGACY_INSTALL=1 fi if [[ -z "$INSTALL_DIRECTORY" \ || "$INSTALL_DIRECTORY" == *$'\n'* \ || "$INSTALL_DIRECTORY" == *$'\r'* ]] then printf '%s\n' "Install directory is empty or contains invalid characters." >&2 exit 2 fi case "$INSTALL_DIRECTORY" in "~") INSTALL_DIRECTORY="$HOME" ;; "~/"*) INSTALL_DIRECTORY="${HOME}/${INSTALL_DIRECTORY:2}" ;; "~"*) printf '%s\n' "Use an absolute path instead of another user's ~ directory." >&2 exit 2 ;; esac if [[ "$INSTALL_DIRECTORY" != /* ]] then INSTALL_DIRECTORY="${PWD}/${INSTALL_DIRECTORY}" fi if command -v realpath >/dev/null 2>&1 then INSTALL_DIRECTORY="$(realpath -m -- "$INSTALL_DIRECTORY")" fi if [[ "$INSTALL_DIRECTORY" == "/" ]] then printf '%s\n' "Refusing to use the filesystem root as the install directory." >&2 exit 2 fi APP_ROOT="${INSTALL_DIRECTORY}/app" RELEASES_ROOT="${APP_ROOT}/releases" RELEASE_ROOT="${RELEASES_ROOT}/${VERSION}" if [[ ! -d "${SOURCE_ROOT}/ptlinux" \ || ! -f "${SOURCE_ROOT}/ptdiag/assbin.py" \ || ! -f "${SOURCE_ROOT}/ptdiag/__init__.py" \ || ! -f "${SOURCE_ROOT}/ptdiag/logs.py" \ || ! -f "${SOURCE_ROOT}/ptlinux/desktop_entry.py" \ || ! -f "${SOURCE_ROOT}/ptlinux/launcher_wrapper.py" \ || ! -f "${SOURCE_ROOT}/ptlinux/self_update.py" \ || ! -f "${SOURCE_ROOT}/ptlinux/launcher-update-public.pem" \ || ! -f "${SOURCE_ROOT}/packaging/eu.pristontale.PTLinux.desktop" \ || ! -f "${SOURCE_ROOT}/packaging/eu.pristontale.PTLinux.png" \ || ! -f "${SOURCE_ROOT}/packaging/pristontale-eu" \ || ! -f "${SOURCE_ROOT}/packaging/install-dependencies.sh" ]] then printf '%s\n' "Installer source is incomplete." >&2 exit 1 fi printf '%s\n' "Installing PristonTale EU Linux launcher for the current user" if (( PRESERVED_LEGACY_INSTALL == 1 )) then printf '%s\n' " Existing installation found; preserving its previous data location." fi printf ' Install data: %s\n' "$INSTALL_DIRECTORY" printf ' Application: %s\n' "$APP_ROOT" printf ' Command: %s\n' "${BIN_HOME}/pristontale-eu" printf ' Desktop: %s\n' "${APPLICATION_HOME}/eu.pristontale.PTLinux.desktop" if (( INSTALL_DEPENDENCIES == 1 )) then # shellcheck source=packaging/install-dependencies.sh source "${SOURCE_ROOT}/packaging/install-dependencies.sh" ptlinux_install_host_dependencies "$DRY_RUN" else printf '%s\n' "Skipping host dependency installation." fi if (( DRY_RUN == 1 )) then exit 0 fi if ! command -v "$PYTHON_BIN" >/dev/null 2>&1 then printf 'Python was not found after dependency installation: %s\n' "$PYTHON_BIN" >&2 exit 1 fi if ! "$PYTHON_BIN" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)' then printf '%s\n' "Python 3.11 or newer is required." >&2 exit 1 fi install -d -m 0755 "$APP_ROOT" "$RELEASES_ROOT" "$BIN_HOME" "$APPLICATION_HOME" "$PIXMAP_HOME" STAGING_RELEASE="${RELEASES_ROOT}/.${VERSION}.$$.staging" if [[ -e "$STAGING_RELEASE" || -L "$STAGING_RELEASE" ]] then printf 'Launcher staging path already exists: %s\n' "$STAGING_RELEASE" >&2 exit 1 fi install -d -m 0755 "$STAGING_RELEASE" install -d -m 0755 "${STAGING_RELEASE}/ptlinux" "${STAGING_RELEASE}/ptdiag" install -m 0644 "${SOURCE_ROOT}/ptlinux/"*.py "${STAGING_RELEASE}/ptlinux/" install -m 0644 \ "${SOURCE_ROOT}/ptlinux/launcher-update-public.pem" \ "${STAGING_RELEASE}/ptlinux/launcher-update-public.pem" install -m 0644 "${SOURCE_ROOT}/ptdiag/__init__.py" "${STAGING_RELEASE}/ptdiag/__init__.py" install -m 0644 "${SOURCE_ROOT}/ptdiag/assbin.py" "${STAGING_RELEASE}/ptdiag/assbin.py" install -m 0644 "${SOURCE_ROOT}/ptdiag/logs.py" "${STAGING_RELEASE}/ptdiag/logs.py" printf '{\n "schema": 1,\n "version": "%s"\n}\n' "$VERSION" > "${STAGING_RELEASE}/release.json" chmod 0644 "${STAGING_RELEASE}/release.json" if ! "$PYTHON_BIN" -I -c \ 'import sys; sys.path.insert(0, sys.argv[1]); import ptlinux, ptlinux.cli, ptlinux.self_update; raise SystemExit(0 if ptlinux.__version__ == sys.argv[2] else 1)' \ "$STAGING_RELEASE" "$VERSION" then printf '%s\n' "The staged launcher failed its import/version check." >&2 rm -r -- "$STAGING_RELEASE" exit 1 fi REPLACED_RELEASE="" if [[ -e "$RELEASE_ROOT" || -L "$RELEASE_ROOT" ]] then if [[ ! -d "$RELEASE_ROOT" || -L "$RELEASE_ROOT" ]] then printf 'Existing launcher release is not a directory: %s\n' "$RELEASE_ROOT" >&2 rm -r -- "$STAGING_RELEASE" exit 1 fi REPLACED_RELEASE="${RELEASES_ROOT}/.${VERSION}.$$.replaced" mv -- "$RELEASE_ROOT" "$REPLACED_RELEASE" fi if ! mv -- "$STAGING_RELEASE" "$RELEASE_ROOT" then if [[ -n "$REPLACED_RELEASE" && -d "$REPLACED_RELEASE" ]] then mv -- "$REPLACED_RELEASE" "$RELEASE_ROOT" fi exit 1 fi if [[ -n "$REPLACED_RELEASE" && -d "$REPLACED_RELEASE" ]] then rm -r -- "$REPLACED_RELEASE" fi CURRENT_TARGET="" if [[ -L "${APP_ROOT}/current" ]] then CURRENT_TARGET="$(readlink -- "${APP_ROOT}/current")" if [[ ! "$CURRENT_TARGET" =~ ^releases/[0-9]+\.[0-9]+\.[0-9]+$ ]] then printf 'Existing launcher current link is unsafe: %s\n' "$CURRENT_TARGET" >&2 exit 1 fi if [[ ! -d "${APP_ROOT}/${CURRENT_TARGET}" ]] then printf 'Existing launcher current link is broken: %s\n' "$CURRENT_TARGET" >&2 exit 1 fi elif [[ -e "${APP_ROOT}/current" ]] then printf 'Launcher current path is not a symbolic link: %s\n' "${APP_ROOT}/current" >&2 exit 1 fi if [[ -L "${APP_ROOT}/previous" ]] then PREVIOUS_TARGET="$(readlink -- "${APP_ROOT}/previous")" if [[ ! "$PREVIOUS_TARGET" =~ ^releases/[0-9]+\.[0-9]+\.[0-9]+$ \ || ! -d "${APP_ROOT}/${PREVIOUS_TARGET}" ]] then printf 'Existing launcher previous link is unsafe or broken: %s\n' "$PREVIOUS_TARGET" >&2 exit 1 fi elif [[ -e "${APP_ROOT}/previous" ]] then printf 'Launcher previous path is not a symbolic link: %s\n' "${APP_ROOT}/previous" >&2 exit 1 fi if [[ -n "$CURRENT_TARGET" && "$CURRENT_TARGET" != "releases/${VERSION}" ]] then PREVIOUS_LINK="${APP_ROOT}/.previous.$$.tmp" ln -s -- "$CURRENT_TARGET" "$PREVIOUS_LINK" mv -Tf -- "$PREVIOUS_LINK" "${APP_ROOT}/previous" fi CURRENT_LINK="${APP_ROOT}/.current.$$.tmp" ln -s -- "releases/${VERSION}" "$CURRENT_LINK" mv -Tf -- "$CURRENT_LINK" "${APP_ROOT}/current" "$PYTHON_BIN" "${RELEASE_ROOT}/ptlinux/launcher_wrapper.py" \ "${SOURCE_ROOT}/packaging/pristontale-eu" \ "$APP_ROOT" \ "$INSTALL_DIRECTORY" \ "${BIN_HOME}/pristontale-eu" install -m 0644 \ "${SOURCE_ROOT}/packaging/eu.pristontale.PTLinux.png" \ "$ICON_PATH" "$PYTHON_BIN" "${RELEASE_ROOT}/ptlinux/desktop_entry.py" \ "${SOURCE_ROOT}/packaging/eu.pristontale.PTLinux.desktop" \ "${BIN_HOME}/pristontale-eu" \ "$ICON_PATH" \ "${APPLICATION_HOME}/eu.pristontale.PTLinux.desktop" if command -v desktop-file-validate >/dev/null 2>&1 then desktop-file-validate "${APPLICATION_HOME}/eu.pristontale.PTLinux.desktop" fi if command -v update-desktop-database >/dev/null 2>&1 then update-desktop-database "$APPLICATION_HOME" >/dev/null 2>&1 || true fi printf '%s\n' "Launcher files installed." DOCTOR_READY=1 if (( RUN_DOCTOR == 1 )) then if ! "${BIN_HOME}/pristontale-eu" doctor then DOCTOR_READY=0 fi else printf '%s\n' "Host readiness check skipped." fi printf '%s\n' "Start PristonTale EU with:" printf ' %q gui\n' "${BIN_HOME}/pristontale-eu" case ":${PATH:-}:" in *":${BIN_HOME}:"*) ;; *) printf '%s\n' \ "Note: ${BIN_HOME} is not in PATH for this shell." \ "The application-menu shortcut already uses the full path." if [[ "${SHELL##*/}" == "fish" ]] then printf '%s\n' "Enable the short command in Fish with:" printf ' fish_add_path %q\n' "$BIN_HOME" else printf '%s\n' "Enable the short command in this Bash/Zsh session with:" printf ' export PATH=%q:"$PATH"\n' "$BIN_HOME" fi ;; esac if (( DOCTOR_READY == 0 )) then printf '%s\n' \ "Launcher files were installed, but this host is not ready to run the game." \ "Resolve the diagnostics above, then run the launcher command again." >&2 exit 1 fi printf '%s\n' "Installation ready."