#!/bin/bash # SPDX-License-Identifier: AGPL-3.0-or-later # Copyright (c) 2025-2026 Cristian Cezar Moisés # ╔════════════════════════════════════════════════════════════════════╗ # ║ ZUPT 2.2.3 + ZUPT-GUI 1.1.1 — UNIVERSAL LINUX INSTALLER ║ # ║ ║ # ║ One script, all distributions. Self-extracting. No internet ║ # ║ needed for the package install (only for Qt6 dependency). ║ # ║ ║ # ║ Usage: sudo bash zupt-installer.sh ║ # ║ Or: sudo bash zupt-installer.sh --gui-only ║ # ║ Or: sudo bash zupt-installer.sh --cli-only ║ # ║ Or: sudo bash zupt-installer.sh --appimage ║ # ║ Or: sudo bash zupt-installer.sh --uninstall ║ # ╚════════════════════════════════════════════════════════════════════╝ set -e VERSION="2.2.3" GUI_VERSION="1.1.1" EXTRACT_DIR="" cleanup() { [ -n "$EXTRACT_DIR" ] && [ -d "$EXTRACT_DIR" ] && rm -rf "$EXTRACT_DIR" } trap cleanup EXIT # ── Color output (if terminal supports) ───────────────────────────── if [ -t 1 ]; then BOLD='\033[1m'; CYAN='\033[36m'; GREEN='\033[32m'; YELLOW='\033[33m'; RED='\033[31m'; RESET='\033[0m' else BOLD=''; CYAN=''; GREEN=''; YELLOW=''; RED=''; RESET='' fi step() { echo -e "${CYAN}${BOLD}═══ $* ═══${RESET}"; } ok() { echo -e "${GREEN}✓${RESET} $*"; } warn() { echo -e "${YELLOW}⚠${RESET} $*"; } err() { echo -e "${RED}✗${RESET} $*" >&2; } die() { err "$*"; exit 1; } # ── Parse arguments ───────────────────────────────────────────────── MODE="full" case "${1:-}" in --cli-only) MODE="cli" ;; --gui-only) MODE="gui" ;; --appimage) MODE="appimage" ;; --uninstall) MODE="uninstall" ;; --help|-h) sed -n '2,15p' "$0" | sed 's/^# //' exit 0 ;; "") MODE="full" ;; *) die "Unknown option: $1. Use --help for options." ;; esac # ── Root check (except for AppImage) ──────────────────────────────── if [ "$MODE" != "appimage" ] && [ "$EUID" -ne 0 ]; then die "Run with sudo: sudo bash $0 ${1:-}" fi # ── Distro detection ──────────────────────────────────────────────── detect_distro() { if [ -f /etc/os-release ]; then # Use subshell to prevent /etc/os-release VERSION from clobbering ours DISTRO=$(. /etc/os-release; echo "${ID:-unknown}") DISTRO_LIKE=$(. /etc/os-release; echo "${ID_LIKE:-}") DISTRO_NAME=$(. /etc/os-release; echo "${PRETTY_NAME:-$DISTRO}") else DISTRO="unknown"; DISTRO_LIKE=""; DISTRO_NAME="Unknown Linux" fi } detect_distro # Categorize DEB_BASED=0; RPM_BASED=0; ARCH_BASED=0; ALPINE=0 case "$DISTRO" in debian|ubuntu|linuxmint|pop|elementary|kali|raspbian|deepin|zorin) DEB_BASED=1 ;; fedora|rhel|centos|rocky|almalinux|ol) RPM_BASED=1 ;; opensuse*|suse|sles) RPM_BASED=1 ;; arch|manjaro|endeavouros|garuda|artix) ARCH_BASED=1 ;; alpine) ALPINE=1 ;; *) case "$DISTRO_LIKE" in *debian*|*ubuntu*) DEB_BASED=1 ;; *fedora*|*rhel*|*suse*) RPM_BASED=1 ;; *arch*) ARCH_BASED=1 ;; esac ;; esac # ── Self-extract embedded payload ─────────────────────────────────── extract_payload() { EXTRACT_DIR=$(mktemp -d -t zupt-installer.XXXXXX) # Find the line number where the payload starts (marker: __PAYLOAD_BELOW__) local marker_line marker_line=$(grep -an '^__PAYLOAD_BELOW__$' "$0" | head -1 | cut -d: -f1) [ -z "$marker_line" ] && die "Installer is corrupt — no payload marker." # Skip past marker line, decode base64 → tar tail -n +$((marker_line + 1)) "$0" | base64 -d | tar -xzC "$EXTRACT_DIR" [ -f "$EXTRACT_DIR/zupt_${VERSION}_amd64.deb" ] || die "Payload extraction failed." } # ── Install Qt6 binding (needs network) ───────────────────────────── install_qt6() { if python3 -c 'import PyQt6.QtWidgets' 2>/dev/null \ || python3 -c 'import PySide6.QtWidgets' 2>/dev/null; then ok "Qt6 binding already installed" return 0 fi step "Installing Python 3 + Qt6 binding" if [ $DEB_BASED -eq 1 ]; then apt-get update -qq || warn "apt-get update failed (network?); continuing anyway" apt-get install -y python3 python3-pyqt6 \ || apt-get install -y python3 python3-pyside6 \ || warn "Could not install Qt6 binding via apt" elif [ $RPM_BASED -eq 1 ]; then case "$DISTRO" in opensuse*|suse|sles) zypper --non-interactive install python3 python3-pyqt6 \ || zypper --non-interactive install python3 python3-PyQt6 \ || zypper --non-interactive install python3 python3-pyside6 ;; *) if command -v dnf >/dev/null; then dnf install -y python3 python3-pyqt6 \ || dnf install -y python3 python3-pyside6 else yum install -y python3 python3-pyqt6 \ || yum install -y python3 python3-pyside6 fi ;; esac elif [ $ARCH_BASED -eq 1 ]; then pacman -Sy --noconfirm python python-pyqt6 \ || pacman -Sy --noconfirm python python-pyside6 elif [ $ALPINE -eq 1 ]; then apk add python3 py3-pyqt6 || apk add python3 py3-pyside6 else warn "Unknown distribution. Trying pip fallback..." if command -v pip3 >/dev/null; then pip3 install --break-system-packages PySide6 2>/dev/null \ || pip3 install --user PySide6 else warn "No pip3. Install python3-pyqt6 manually." fi fi if python3 -c 'import PyQt6.QtWidgets' 2>/dev/null \ || python3 -c 'import PySide6.QtWidgets' 2>/dev/null; then ok "Qt6 binding installed" else warn "Qt6 binding install failed. The CLI will still work; the GUI won't." fi } # ── Install zupt CLI ──────────────────────────────────────────────── install_cli() { step "Installing zupt CLI ${VERSION}" if [ $DEB_BASED -eq 1 ]; then dpkg -i "$EXTRACT_DIR/zupt_${VERSION}_amd64.deb" 2>&1 \ | grep -v '^Selecting\|^Preparing\|^Unpacking\|^Setting up\|^Processing' || true # Resolve any missing libs from apt apt-get -f install -y 2>/dev/null || true ok "zupt CLI installed: $(zupt version 2>&1 | head -1)" elif [ $RPM_BASED -eq 1 ]; then local rpmtar="$EXTRACT_DIR/zupt-${VERSION}.srpm.tar.gz" if command -v rpmbuild >/dev/null; then local rpmroot=$(mktemp -d) tar -xzC "$rpmroot" -f "$rpmtar" rpmbuild --define "_topdir $rpmroot" -bb "$rpmroot/SPECS/zupt.spec" rpm -Uvh --force "$rpmroot"/RPMS/x86_64/zupt-*.rpm rm -rf "$rpmroot" else # rpmbuild not available — fall back to tarball warn "rpmbuild missing — using portable binary install" local appdir="$EXTRACT_DIR/zupt-${VERSION}-x86_64.AppDir.tar.gz" mkdir -p /opt tar -xzC /opt -f "$appdir" ln -sf "/opt/zupt-${VERSION}-x86_64.AppDir/AppRun" /usr/local/bin/zupt ok "zupt CLI installed (portable mode)" fi else # Universal fallback: portable AppDir tarball warn "No native package format for $DISTRO. Using portable binary." mkdir -p /opt /usr/local/bin tar -xzC /opt -f "$EXTRACT_DIR/zupt-${VERSION}-x86_64.AppDir.tar.gz" ln -sf "/opt/zupt-${VERSION}-x86_64.AppDir/AppRun" /usr/local/bin/zupt ok "zupt CLI installed (portable mode)" fi } # ── Install zupt-gui ──────────────────────────────────────────────── install_gui() { step "Installing zupt-gui ${GUI_VERSION}" if [ $DEB_BASED -eq 1 ]; then dpkg -i "$EXTRACT_DIR/zupt-gui_${GUI_VERSION}_all.deb" 2>&1 \ | grep -v '^Selecting\|^Preparing\|^Unpacking\|^Setting up\|^Processing' || true apt-get -f install -y 2>/dev/null || true ok "zupt-gui installed" elif [ $RPM_BASED -eq 1 ]; then local rpmtar="$EXTRACT_DIR/zupt-gui-${GUI_VERSION}.srpm.tar.gz" if command -v rpmbuild >/dev/null; then local rpmroot=$(mktemp -d) tar -xzC "$rpmroot" -f "$rpmtar" rpmbuild --define "_topdir $rpmroot" -bb "$rpmroot/SPECS/zupt-gui.spec" rpm -Uvh --force "$rpmroot"/RPMS/noarch/zupt-gui-*.rpm rm -rf "$rpmroot" else warn "rpmbuild missing — using portable mode" mkdir -p /opt /usr/local/bin tar -xzC /opt -f "$EXTRACT_DIR/Zupt-GUI-${GUI_VERSION}-x86_64.AppDir.tar.gz" ln -sf "/opt/zupt-gui.AppDir/AppRun" /usr/local/bin/zupt-gui ok "zupt-gui installed (portable)" fi else # Portable mkdir -p /opt /usr/local/bin tar -xzC /opt -f "$EXTRACT_DIR/Zupt-GUI-${GUI_VERSION}-x86_64.AppDir.tar.gz" ln -sf "/opt/zupt-gui.AppDir/AppRun" /usr/local/bin/zupt-gui # Desktop integration if possible if [ -d /usr/share/applications ]; then cp /opt/zupt-gui.AppDir/zupt-gui.desktop /usr/share/applications/ 2>/dev/null || true fi ok "zupt-gui installed (portable)" fi } # ── AppImage extract (no install) ─────────────────────────────────── install_appimage() { step "Extracting AppImage to current directory" local target="${PWD}/zupt-portable" mkdir -p "$target" tar -xzC "$target" -f "$EXTRACT_DIR/zupt-${VERSION}-x86_64.AppDir.tar.gz" tar -xzC "$target" -f "$EXTRACT_DIR/Zupt-GUI-${GUI_VERSION}-x86_64.AppDir.tar.gz" cat > "$target/zupt" < "$target/zupt-gui" </dev/null || true dpkg -r zupt 2>/dev/null || true elif [ $RPM_BASED -eq 1 ]; then rpm -e zupt-gui 2>/dev/null || true rpm -e zupt 2>/dev/null || true fi rm -rf /opt/zupt-2.2.3-x86_64.AppDir /opt/zupt-gui.AppDir 2>/dev/null rm -f /usr/local/bin/zupt /usr/local/bin/zupt-gui 2>/dev/null rm -f /usr/share/applications/zupt-gui.desktop 2>/dev/null ok "Uninstall complete" } # ───────────────────────────────────────────────────────────────────── # MAIN # ───────────────────────────────────────────────────────────────────── cat <