#!/usr/bin/env bash # ============================================================================== # SimpleDZ — Linux Installer and Uninstaller # # Safe, POSIX/bash compliant installer for SimpleDZ desktop application. # Runs entirely in user space (~/.local/share/simpledz, ~/.local/bin). # Does NOT require sudo / root privileges. # # Directory Layout: # Program: ~/.local/share/simpledz/ (binary, DLLs, IntegrationTools) # Configuration: ~/.config/simpledz/ (settings.json, session.dat/key) # Data: ~/SimpleDZ/ (Servers, Backups, Logs, Cache) # Launcher: ~/.local/bin/simpledz (Symlink) # # Usage: # curl -fsSL https://ksdev.ovh/dl/install.sh | bash # ./install.sh [OPTIONS] # # Options: # -h, --help Show this help message and exit # --uninstall Remove application files, desktop entry, and icon. # Preserves user data and configuration. # --purge Completely remove all SimpleDZ data including servers, # backups, and configurations (prompts for confirmation). # -y, --yes Assume 'yes' to prompts (e.g. during --purge). # --version Install specific version instead of latest/default. # ============================================================================== set -euo pipefail # ------------------------------------------------------------------------------ # Configuration & Directory Paths # ------------------------------------------------------------------------------ readonly APP_NAME="SimpleDZ" readonly BINARY_NAME="simpledz" readonly BASE_URL="${SIMPLE_DZ_BASE_URL:-https://ksdev.ovh/dl}" # These placeholders may be substituted by the build script (build-linux.sh) DEFAULT_VERSION="0.1.0-alpha.2-preview" DEFAULT_SHA256="f648635836154869ddbc6d58372f3421a817ba4cd4c7ddfebee3027537c144dd" # User-specific directory paths (XDG compliant) readonly XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" readonly XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" readonly APP_DIR="${XDG_DATA_HOME}/simpledz" readonly CONFIG_DIR="${XDG_CONFIG_HOME}/simpledz" readonly DEFAULT_DATA_DIR="${HOME}/SimpleDZ" readonly BIN_DIR="${HOME}/.local/bin" readonly BIN_LINK="${BIN_DIR}/simpledz" readonly DESKTOP_DIR="${XDG_DATA_HOME}/applications" readonly DESKTOP_FILE="${DESKTOP_DIR}/simpledz.desktop" readonly ICON_DIR="${XDG_DATA_HOME}/icons/hicolor/256x256/apps" readonly ICON_FILE="${ICON_DIR}/simpledz.png" # Flags FORCE_YES=0 TARGET_VERSION="" tmp_dir="" cleanup() { if [ -n "${tmp_dir:-}" ] && [ -d "${tmp_dir}" ]; then rm -rf -- "${tmp_dir}" fi } trap cleanup EXIT INT TERM # ------------------------------------------------------------------------------ # Output formatting helpers # ------------------------------------------------------------------------------ if [ -t 1 ]; then C_RESET="$(printf '\033[0m')" C_BOLD="$(printf '\033[1m')" C_GREEN="$(printf '\033[32m')" C_YELLOW="$(printf '\033[33m')" C_RED="$(printf '\033[31m')" C_CYAN="$(printf '\033[36m')" else C_RESET='' C_BOLD='' C_GREEN='' C_YELLOW='' C_RED='' C_CYAN='' fi info() { printf "${C_CYAN}[INFO]${C_RESET} %s\n" "$*" } success() { printf "${C_GREEN}${C_BOLD}[OK]${C_RESET} %s\n" "$*" } warn() { printf "${C_YELLOW}${C_BOLD}[WARN]${C_RESET} %s\n" "$*" >&2 } error() { printf "${C_RED}${C_BOLD}[ERROR]${C_RESET} %s\n" "$*" >&2 } # ------------------------------------------------------------------------------ # Data Directory Resolver # ------------------------------------------------------------------------------ get_data_dir() { local settings_file="${CONFIG_DIR}/settings.json" local raw_data_dir="" if [ -f "$settings_file" ]; then if command -v python3 >/dev/null 2>&1; then raw_data_dir="$(python3 - "$settings_file" <<'PY' 2>/dev/null || true import json import sys try: with open(sys.argv[1], 'r', encoding='utf-8') as f: data = json.load(f) print(data.get('DataDirectory') or data.get('dataDirectory') or '') except Exception: pass PY )" fi if [ -z "$raw_data_dir" ]; then # Fallback to grep/sed if python3 is unavailable or failed raw_data_dir="$(grep -i '"dataDirectory"' "$settings_file" 2>/dev/null | head -n 1 | sed -E 's/.*"[Dd]ataDirectory"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/' || true)" fi fi if [ -n "$raw_data_dir" ]; then # Expand leading ~ to $HOME case "$raw_data_dir" in "~") raw_data_dir="${HOME}" ;; "~/"*) raw_data_dir="${HOME}/${raw_data_dir#"~/"}" ;; esac echo "$raw_data_dir" else echo "${DEFAULT_DATA_DIR}" fi } # ------------------------------------------------------------------------------ # Path Validation Safety Check # ------------------------------------------------------------------------------ validate_safe_path() { local raw_target="${1:-}" local desc="${2:-Target path}" if [ -z "$raw_target" ]; then error "Safety check failed: ${desc} is empty." exit 1 fi local target="" if command -v realpath >/dev/null 2>&1; then target="$(realpath -m -- "$raw_target" 2>/dev/null || true)" fi if [ -z "$target" ] && command -v python3 >/dev/null 2>&1; then target="$(python3 -c "import os, sys; print(os.path.abspath(sys.argv[1]))" "$raw_target" 2>/dev/null || true)" fi if [ -z "$target" ]; then target="${raw_target}" fi [ "$target" != "/" ] && target="${target%/}" case "$target" in /*) ;; *) error "Safety check failed: ${desc} must be an absolute path: '${raw_target}'" exit 1 ;; esac if [ "$target" = "/" ]; then error "Safety check failed: ${desc} cannot be the root directory ('/')." exit 1 fi local norm_home="" if command -v realpath >/dev/null 2>&1; then norm_home="$(realpath -m -- "${HOME}" 2>/dev/null || true)" fi if [ -z "$norm_home" ] && command -v python3 >/dev/null 2>&1; then norm_home="$(python3 -c "import os, sys; print(os.path.abspath(sys.argv[1]))" "${HOME}" 2>/dev/null || true)" fi if [ -z "$norm_home" ]; then norm_home="${HOME}" fi [ "$norm_home" != "/" ] && norm_home="${norm_home%/}" if [ -z "${norm_home}" ]; then error "Safety check failed: HOME directory is empty or not set." exit 1 fi if [ "$target" = "$norm_home" ] || [ "$target" = "${HOME}" ]; then error "Safety check failed: ${desc} cannot be user HOME directory: '${raw_target}'" exit 1 fi case "$target" in "${norm_home}/"*) ;; *) error "Safety check failed: ${desc} must be located inside user HOME directory ('${norm_home}/...'): '${raw_target}'" exit 1 ;; esac case "$target" in "${norm_home}/.local" | \ "${norm_home}/.local/share" | \ "${norm_home}/.local/bin" | \ "${norm_home}/.config" | \ "${norm_home}/.cache" | \ "${norm_home}/Desktop" | \ "${norm_home}/Documents" | \ "${norm_home}/Downloads" | \ "${norm_home}/Pulpit" | \ "${norm_home}/Dokumenty" | \ "${norm_home}/Pobrane") error "Safety check failed: ${desc} cannot be a protected system or standard user directory: '${raw_target}'" exit 1 ;; esac } # ------------------------------------------------------------------------------ # Help message # ------------------------------------------------------------------------------ show_help() { cat << EOF ${C_BOLD}${APP_NAME} — Linux Installer & Manager${C_RESET} ${C_BOLD}USAGE:${C_RESET} curl -fsSL https://ksdev.ovh/dl/install.sh | bash ./install.sh [OPTIONS] ${C_BOLD}OPTIONS:${C_RESET} -h, --help Show this help message and exit --uninstall Remove application binaries, desktop entry, and icon. Preserves user data and configuration. --purge Completely delete all SimpleDZ data, including server files, backups, and configurations (prompts for confirmation). -y, --yes Assume 'yes' to prompts (non-interactive confirmation). --version Specify a version to install instead of latest. ${C_BOLD}PATHS:${C_RESET} Program: ${APP_DIR} Configuration: ${CONFIG_DIR} Data: $(get_data_dir) Launcher: ${BIN_LINK} Desktop entry: ${DESKTOP_FILE} Icon: ${ICON_FILE} EOF } # ------------------------------------------------------------------------------ # Dependency & Architecture Checks # ------------------------------------------------------------------------------ check_architecture() { local arch arch="$(uname -m)" if [ "$arch" != "x86_64" ]; then error "${APP_NAME} currently supports only x86_64 architecture. Detected: ${arch}" exit 1 fi } check_root() { if [ "${EUID:-$(id -u)}" -eq 0 ]; then warn "Running installer as root is not recommended. ${APP_NAME} is installed" warn "into user home directory (${APP_DIR}) without root privileges." fi } check_gui_libraries() { local missing=() local libs=("libICE.so.6" "libSM.so.6" "libfontconfig.so.1" "libX11.so.6") local ldconfig_bin="" for cand in ldconfig /sbin/ldconfig /usr/sbin/ldconfig; do if command -v "$cand" >/dev/null 2>&1; then ldconfig_bin="$cand" break fi done for lib in "${libs[@]}"; do local found=0 if [ -n "$ldconfig_bin" ] && "$ldconfig_bin" -p 2>/dev/null | grep -qF "$lib"; then found=1 else for d in /usr/lib /usr/lib64 /usr/lib/x86_64-linux-gnu /lib/x86_64-linux-gnu /lib64; do if [ -e "${d}/${lib}" ]; then found=1 break fi done fi if [ "$found" -eq 0 ]; then missing+=("$lib") fi done if [ ${#missing[@]} -gt 0 ]; then warn "The following GUI runtime libraries were not detected: ${missing[*]}" warn "If ${APP_NAME} fails to start a graphical window, install them via your package manager:" warn " Ubuntu/Debian: sudo apt install libice6 libsm6 libfontconfig1 libx11-6" warn " Fedora/RHEL: sudo dnf install libICE libSM fontconfig libX11" warn " Arch Linux: sudo pacman -S libice libsm fontconfig libx11" fi } # ------------------------------------------------------------------------------ # Cache and Database Update Helpers # ------------------------------------------------------------------------------ update_system_caches() { if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database "${DESKTOP_DIR}" >/dev/null 2>&1 || true fi if command -v gtk-update-icon-cache >/dev/null 2>&1; then gtk-update-icon-cache -f -q -t "${XDG_DATA_HOME}/icons/hicolor" >/dev/null 2>&1 || true fi } # ------------------------------------------------------------------------------ # Uninstall procedures # ------------------------------------------------------------------------------ uninstall_app() { local purge="${1:-0}" info "Starting uninstallation of ${APP_NAME}..." # Always remove symlink, desktop entry, and icon rm -f "${BIN_LINK}" rm -f "${DESKTOP_FILE}" rm -f "${ICON_FILE}" update_system_caches if [ "$purge" -eq 1 ]; then local data_dir data_dir="$(get_data_dir)" if [ "$FORCE_YES" -ne 1 ]; then printf "${C_RED}${C_BOLD}WARNING:${C_RESET} This will permanently delete ALL ${APP_NAME} data,\n" printf "including server configurations, game files, backups, and login sessions in:\n" printf ' • %-15s %s\n' "Program:" "${APP_DIR}" printf ' • %-15s %s\n' "Configuration:" "${CONFIG_DIR}" printf ' • %-15s %s\n' "Data:" "${data_dir}" printf "Are you sure you want to proceed? [y/N]: " local reply="" if [ -t 0 ]; then read -r reply elif [ -e /dev/tty ] && [ -r /dev/tty ]; then read -r reply /dev/null | tr -d '\r\n[:space:]')"; then error "Failed to retrieve version information from ${BASE_URL}/latest.txt" error "Please check your network connection or specify --version " exit 1 fi fi fi if [ -z "$version" ]; then error "Could not determine version to install." exit 1 fi local archive_name="simpledz-${version}-linux-x64.tar.gz" local archive_url="${BASE_URL}/${archive_name}" local sha256_url="${archive_url}.sha256" info "Installing ${APP_NAME} version ${version}..." # Create secure temporary directory with automatic cleanup trap tmp_dir="$(mktemp -d -t simpledz-install-XXXXXX)" local archive_file="${tmp_dir}/${archive_name}" info "Downloading package from: ${archive_url}" if ! curl -fsSL --proto '=https' --tlsv1.2 -o "${archive_file}" "${archive_url}"; then error "Failed to download ${archive_name} from ${archive_url}" exit 1 fi # Determine expected SHA-256 local expected_sha256="" if [ -n "${DEFAULT_SHA256}" ] && [ "${DEFAULT_SHA256}" != "__SHA256__" ] && [ -z "${TARGET_VERSION}" ]; then expected_sha256="${DEFAULT_SHA256}" else info "Fetching checksum from: ${sha256_url}" local sha256_file="${tmp_dir}/${archive_name}.sha256" if ! curl -fsSL --proto '=https' --tlsv1.2 -o "${sha256_file}" "${sha256_url}"; then error "Failed to download SHA-256 checksum file: ${sha256_url}" exit 1 fi expected_sha256="$(awk '{print $1}' "${sha256_file}" | tr -d '\r\n[:space:]')" fi if [ -z "$expected_sha256" ]; then error "Expected SHA-256 checksum is empty or invalid." exit 1 fi info "Verifying SHA-256 checksum before extracting..." local actual_sha256 actual_sha256="$(sha256sum "${archive_file}" | awk '{print $1}' | tr -d '\r\n[:space:]')" if [ "$actual_sha256" != "$expected_sha256" ]; then error "SHA-256 checksum verification failed!" error "Expected: ${expected_sha256}" error "Actual: ${actual_sha256}" exit 1 fi success "SHA-256 checksum verified: ${actual_sha256}" # Extract package into temporary folder local extract_dir="${tmp_dir}/extracted" mkdir -p "${extract_dir}" tar -xzf "${archive_file}" -C "${extract_dir}" local src_dir="${extract_dir}/simpledz" if [ ! -d "${src_dir}" ]; then src_dir="${extract_dir}" fi if [ ! -f "${src_dir}/${BINARY_NAME}" ]; then error "Corrupted package: '${BINARY_NAME}' not found in archive." exit 1 fi # Create installation destination directories mkdir -p "${BIN_DIR}" mkdir -p "${DESKTOP_DIR}" mkdir -p "${ICON_DIR}" # Prepare clean application directory (it contains ONLY the program) if [ -d "${APP_DIR}" ]; then validate_safe_path "${APP_DIR}" "Application directory" rm -rf "${APP_DIR:?}"/* 2>/dev/null || true fi mkdir -p "${APP_DIR}" info "Installing files to ${APP_DIR}..." cp -rf "${src_dir}"/* "${APP_DIR}/" # Ensure binary has executable rights chmod +x "${APP_DIR}/${BINARY_NAME}" # Create runner symlink ~/.local/bin/simpledz ln -sf "${APP_DIR}/${BINARY_NAME}" "${BIN_LINK}" info "Created launcher symlink: ${BIN_LINK}" # Install desktop icon if [ -f "${src_dir}/simpledz.png" ]; then cp -f "${src_dir}/simpledz.png" "${ICON_FILE}" info "Installed application icon: ${ICON_FILE}" fi # Create desktop shortcut cat > "${DESKTOP_FILE}" << EOF [Desktop Entry] Type=Application Name=SimpleDZ Comment=The easy way to run DayZ servers Exec=${BIN_LINK} Icon=simpledz Terminal=false Categories=Game;Utility; StartupWMClass=simpledz EOF info "Created desktop entry: ${DESKTOP_FILE}" # Validate desktop file if validator tool is installed if command -v desktop-file-validate >/dev/null 2>&1; then desktop-file-validate "${DESKTOP_FILE}" >/dev/null 2>&1 || true fi # Update system caches update_system_caches # Verify PATH local in_path=0 case ":${PATH}:" in *":${BIN_DIR}:"*) in_path=1 ;; esac local data_dir data_dir="$(get_data_dir)" printf "\n" success "${APP_NAME} ${version} has been successfully installed!" printf "\n" printf "${C_BOLD}Installed Locations:${C_RESET}\n" printf ' • %-15s %s\n' "Program:" "${APP_DIR}" printf ' • %-15s %s\n' "Configuration:" "${CONFIG_DIR}" printf ' • %-15s %s\n' "Data:" "${data_dir}" printf ' • %-15s %s\n' "Launcher:" "${BIN_LINK}" printf ' • %-15s %s\n' "Desktop Entry:" "${DESKTOP_FILE}" printf "\n" if [ "$in_path" -eq 0 ]; then warn "'${BIN_DIR}' is not in your current PATH." warn "To run '${BINARY_NAME}' directly from terminal, add this directory to PATH:" printf " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.bashrc\n" printf " source ~/.bashrc\n\n" fi printf "${C_BOLD}Useful Commands:${C_RESET}\n" printf " • Update: curl -fsSL https://ksdev.ovh/dl/install.sh | bash\n" printf " • Uninstall: curl -fsSL https://ksdev.ovh/dl/install.sh | bash -s -- --uninstall\n" printf " • Purge: curl -fsSL https://ksdev.ovh/dl/install.sh | bash -s -- --purge\n" printf " • Docs: https://ksdev.ovh/docs#install\n" printf "\n" info "You can launch ${APP_NAME} from your application menu or by running:" printf " %s\n\n" "${BIN_LINK}" } # ------------------------------------------------------------------------------ # Argument Parsing & Entry Point # ------------------------------------------------------------------------------ main() { local action="install" local purge=0 while [ $# -gt 0 ]; do case "$1" in -h|--help) show_help exit 0 ;; --uninstall) action="uninstall" shift ;; --purge) action="uninstall" purge=1 shift ;; -y|--yes) FORCE_YES=1 shift ;; --version) if [ $# -lt 2 ]; then error "Option --version requires a version argument." exit 1 fi TARGET_VERSION="$2" shift 2 ;; *) error "Unknown option: $1" show_help exit 1 ;; esac done case "$action" in uninstall) uninstall_app "$purge" ;; install) install_app ;; esac } main "$@"