From 7a283169b8a6bede18cebe16e8909c79c9d76c24 Mon Sep 17 00:00:00 2001 From: Jatin K Malik Date: Mon, 23 Jun 2025 15:02:51 -0500 Subject: [PATCH] Improve installation scripts with better platform detection, dependency management, and error handling (#29) --- install.sh | 781 +++++++++++++++++++++++++++++++++++---- setup.py | 98 +++-- src/vocalinux/version.py | 9 + uninstall.sh | 315 +++++++++++++--- 4 files changed, 1043 insertions(+), 160 deletions(-) create mode 100644 src/vocalinux/version.py diff --git a/install.sh b/install.sh index 1252ffb..ff71c21 100755 --- a/install.sh +++ b/install.sh @@ -78,27 +78,205 @@ if [ "$EUID" -eq 0 ]; then exit 1 fi -# Check Ubuntu version -if [ -f /etc/os-release ]; then - . /etc/os-release - if [[ "$NAME" != *"Ubuntu"* ]]; then - print_warning "This installer is designed for Ubuntu. Your system: $NAME" - echo "The application may still work, but you might need to install dependencies manually." +# Detect Linux distribution and version +detect_distro() { + if [ -f /etc/os-release ]; then + . /etc/os-release + DISTRO_NAME="$NAME" + DISTRO_ID="$ID" + DISTRO_VERSION="$VERSION_ID" + DISTRO_FAMILY="unknown" + + # Determine distribution family + if [[ "$ID" == "ubuntu" || "$ID_LIKE" == *"ubuntu"* || "$ID" == "pop" || "$ID" == "linuxmint" || "$ID" == "elementary" || "$ID" == "zorin" ]]; then + DISTRO_FAMILY="ubuntu" + elif [[ "$ID" == "debian" || "$ID_LIKE" == *"debian"* ]]; then + DISTRO_FAMILY="debian" + elif [[ "$ID" == "fedora" || "$ID_LIKE" == *"fedora"* || "$ID" == "rhel" || "$ID" == "centos" || "$ID" == "rocky" || "$ID" == "almalinux" ]]; then + DISTRO_FAMILY="fedora" + elif [[ "$ID" == "arch" || "$ID_LIKE" == *"arch"* || "$ID" == "manjaro" || "$ID" == "endeavouros" ]]; then + DISTRO_FAMILY="arch" + elif [[ "$ID" == "opensuse" || "$ID_LIKE" == *"suse"* ]]; then + DISTRO_FAMILY="suse" + fi + + print_info "Detected: $DISTRO_NAME $DISTRO_VERSION ($DISTRO_FAMILY family)" + return 0 + else + print_error "Could not detect Linux distribution (missing /etc/os-release)" + return 1 + fi +} + +# Check minimum required version for Ubuntu-based systems +check_ubuntu_version() { + local MIN_VERSION="18.04" + if [[ "$DISTRO_FAMILY" == "ubuntu" ]]; then + if [[ $(echo -e "$DISTRO_VERSION\n$MIN_VERSION" | sort -V | head -n1) == "$MIN_VERSION" || "$DISTRO_VERSION" == "$MIN_VERSION" ]]; then + return 0 + else + print_error "This application requires Ubuntu $MIN_VERSION or newer. Detected: $DISTRO_VERSION" + return 1 + fi + fi + return 0 +} + +# Detect distribution +detect_distro + +# Check compatibility +if [[ "$DISTRO_FAMILY" != "ubuntu" ]]; then + print_warning "This installer is primarily designed for Ubuntu-based systems. Your system: $DISTRO_NAME" + print_warning "The application may still work, but you might need to install dependencies manually." + read -p "Do you want to continue anyway? (y/n) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +else + # Check version for Ubuntu-based systems + if ! check_ubuntu_version; then read -p "Do you want to continue anyway? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi - else - print_info "Detected Ubuntu $VERSION_ID" fi fi +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Function to check if a package is installed (for apt-based systems) +apt_package_installed() { + dpkg -s "$1" >/dev/null 2>&1 +} + +# Function to check if a package is installed (for dnf-based systems) +dnf_package_installed() { + rpm -q "$1" >/dev/null 2>&1 +} + +# Function to check if a package is installed (for pacman-based systems) +pacman_package_installed() { + pacman -Q "$1" >/dev/null 2>&1 +} + +# Function to install system dependencies based on the detected distribution +install_system_dependencies() { + print_info "Installing system dependencies..." + + # Define package names for different distributions + local APT_PACKAGES="python3-pip python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-appindicator3-0.1 libgirepository1.0-dev python3-dev portaudio19-dev python3-venv" + local DNF_PACKAGES="python3-pip python3-gobject gtk3 libappindicator-gtk3 gobject-introspection-devel python3-devel portaudio-devel python3-virtualenv" + local PACMAN_PACKAGES="python-pip python-gobject gtk3 libappindicator-gtk3 gobject-introspection python-cairo portaudio python-virtualenv" + local ZYPPER_PACKAGES="python3-pip python3-gobject python3-gobject-cairo gtk3 libappindicator-gtk3 gobject-introspection-devel python3-devel portaudio-devel python3-virtualenv" + + local MISSING_PACKAGES="" + local INSTALL_CMD="" + local UPDATE_CMD="" + + case "$DISTRO_FAMILY" in + ubuntu|debian) + # Check for missing packages + for pkg in $APT_PACKAGES; do + if ! apt_package_installed "$pkg"; then + MISSING_PACKAGES="$MISSING_PACKAGES $pkg" + fi + done + + if [ -n "$MISSING_PACKAGES" ]; then + print_info "Installing missing packages:$MISSING_PACKAGES" + sudo apt update || { print_error "Failed to update package lists"; exit 1; } + sudo apt install -y $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; } + else + print_info "All required packages are already installed." + fi + ;; + + fedora) + # For Fedora/RHEL-based systems + if command_exists dnf; then + INSTALL_CMD="sudo dnf install -y" + UPDATE_CMD="sudo dnf check-update" + elif command_exists yum; then + INSTALL_CMD="sudo yum install -y" + UPDATE_CMD="sudo yum check-update" + else + print_error "No supported package manager found (dnf/yum)" + exit 1 + fi + + # Check for missing packages + for pkg in $DNF_PACKAGES; do + if ! dnf_package_installed "$pkg"; then + MISSING_PACKAGES="$MISSING_PACKAGES $pkg" + fi + done + + if [ -n "$MISSING_PACKAGES" ]; then + print_info "Installing missing packages:$MISSING_PACKAGES" + $UPDATE_CMD + $INSTALL_CMD $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; } + else + print_info "All required packages are already installed." + fi + ;; + + arch) + # For Arch-based systems + if ! command_exists pacman; then + print_error "Pacman package manager not found" + exit 1 + fi + + # Check for missing packages + for pkg in $PACMAN_PACKAGES; do + if ! pacman_package_installed "$pkg"; then + MISSING_PACKAGES="$MISSING_PACKAGES $pkg" + fi + done + + if [ -n "$MISSING_PACKAGES" ]; then + print_info "Installing missing packages:$MISSING_PACKAGES" + sudo pacman -Sy + sudo pacman -S --noconfirm $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; } + else + print_info "All required packages are already installed." + fi + ;; + + suse) + # For openSUSE + if ! command_exists zypper; then + print_error "Zypper package manager not found" + exit 1 + fi + + print_info "Updating package lists and installing dependencies..." + sudo zypper refresh + sudo zypper install -y $ZYPPER_PACKAGES || { print_error "Failed to install dependencies"; exit 1; } + ;; + + *) + print_error "Unsupported distribution family: $DISTRO_FAMILY" + print_warning "Please install the following dependencies manually:" + print_warning "- Python 3.8 or newer with pip" + print_warning "- PyGObject (GTK3 bindings for Python)" + print_warning "- GTK3 development libraries" + print_warning "- AppIndicator3 support" + print_warning "- PortAudio development libraries" + print_warning "- Python virtual environment support" + read -p "Press Enter to continue once dependencies are installed, or Ctrl+C to cancel..." + ;; + esac +} + # Install system dependencies -print_info "Installing system dependencies..." -sudo apt update -sudo apt install -y python3-pip python3-gi python3-gi-cairo gir1.2-gtk-3.0 \ - gir1.2-appindicator3-0.1 libgirepository1.0-dev python3-dev portaudio19-dev python3-venv +install_system_dependencies # Define XDG directories CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/vocalinux" @@ -106,14 +284,134 @@ DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/vocalinux" DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications" ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/scalable/apps" -# Detect desktop environment for text input tools -if [ "$XDG_SESSION_TYPE" = "wayland" ]; then - print_info "Detected Wayland session, installing wtype..." - sudo apt install -y wtype -else - print_info "Detected X11 session, installing xdotool..." - sudo apt install -y xdotool -fi +# Function to detect and install text input tools +install_text_input_tools() { + # Detect session type more robustly + local SESSION_TYPE="unknown" + + # Check XDG_SESSION_TYPE first + if [ -n "$XDG_SESSION_TYPE" ]; then + SESSION_TYPE="$XDG_SESSION_TYPE" + # Check for Wayland-specific environment variables + elif [ -n "$WAYLAND_DISPLAY" ]; then + SESSION_TYPE="wayland" + # Check if X server is running + elif [ -n "$DISPLAY" ] && command_exists xset && xset q &>/dev/null; then + SESSION_TYPE="x11" + # Check loginctl if available + elif command_exists loginctl; then + SESSION_TYPE=$(loginctl show-session $(loginctl | grep $(whoami) | awk '{print $1}') -p Type | cut -d= -f2) + fi + + print_info "Detected session type: $SESSION_TYPE" + + # Install appropriate tools based on session type and distribution + case "$SESSION_TYPE" in + wayland) + print_info "Installing Wayland text input tools..." + case "$DISTRO_FAMILY" in + ubuntu|debian) + if ! apt_package_installed "wtype"; then + sudo apt install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } + else + print_info "wtype is already installed." + fi + ;; + fedora) + if command_exists dnf && ! dnf_package_installed "wtype"; then + sudo dnf install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } + elif command_exists yum && ! rpm -q wtype &>/dev/null; then + sudo yum install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } + else + print_info "wtype is already installed." + fi + ;; + arch) + if ! pacman_package_installed "wtype"; then + sudo pacman -S --noconfirm wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } + else + print_info "wtype is already installed." + fi + ;; + suse) + sudo zypper install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; } + ;; + *) + print_warning "Unsupported distribution for Wayland text input tools." + print_warning "Please install 'wtype' manually for Wayland text input support." + ;; + esac + ;; + + x11|"") + print_info "Installing X11 text input tools..." + case "$DISTRO_FAMILY" in + ubuntu|debian) + if ! apt_package_installed "xdotool"; then + sudo apt install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } + else + print_info "xdotool is already installed." + fi + ;; + fedora) + if command_exists dnf && ! dnf_package_installed "xdotool"; then + sudo dnf install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } + elif command_exists yum && ! rpm -q xdotool &>/dev/null; then + sudo yum install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } + else + print_info "xdotool is already installed." + fi + ;; + arch) + if ! pacman_package_installed "xdotool"; then + sudo pacman -S --noconfirm xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } + else + print_info "xdotool is already installed." + fi + ;; + suse) + sudo zypper install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; } + ;; + *) + print_warning "Unsupported distribution for X11 text input tools." + print_warning "Please install 'xdotool' manually for X11 text input support." + ;; + esac + ;; + + *) + print_warning "Unknown session type: $SESSION_TYPE" + print_warning "Installing both Wayland and X11 text input tools for compatibility..." + + # Install both tools based on distribution + case "$DISTRO_FAMILY" in + ubuntu|debian) + sudo apt install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; } + ;; + fedora) + if command_exists dnf; then + sudo dnf install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; } + elif command_exists yum; then + sudo yum install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; } + fi + ;; + arch) + sudo pacman -S --noconfirm xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; } + ;; + suse) + sudo zypper install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; } + ;; + *) + print_warning "Unsupported distribution for text input tools." + print_warning "Please install 'xdotool' and 'wtype' manually for text input support." + ;; + esac + ;; + esac +} + +# Install text input tools based on session type +install_text_input_tools # Create necessary directories print_info "Creating application directories..." @@ -122,17 +420,76 @@ mkdir -p "$DATA_DIR/models" mkdir -p "$DESKTOP_DIR" mkdir -p "$ICON_DIR" -# Set up virtual environment -print_info "Setting up Python virtual environment..." -python3 -m venv "$VENV_DIR" --system-site-packages +# Check Python version +check_python_version() { + local MIN_VERSION="3.8" + local PYTHON_CMD="python3" + + # Check if python3 command exists + if ! command_exists python3; then + print_error "Python 3 is not installed or not in PATH" + return 1 + fi + + # Get Python version + local PY_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") + print_info "Detected Python version: $PY_VERSION" + + # Compare versions + if [[ $(echo -e "$PY_VERSION\n$MIN_VERSION" | sort -V | head -n1) == "$MIN_VERSION" || "$PY_VERSION" == "$MIN_VERSION" ]]; then + return 0 + else + print_error "This application requires Python $MIN_VERSION or newer. Detected: $PY_VERSION" + return 1 + fi +} -# Activate virtual environment -source "$VENV_DIR/bin/activate" +# Set up virtual environment with error handling +setup_virtual_environment() { + print_info "Setting up Python virtual environment in $VENV_DIR..." + + # Check if virtual environment already exists + if [ -d "$VENV_DIR" ] && [ -f "$VENV_DIR/bin/activate" ]; then + print_warning "Virtual environment already exists in $VENV_DIR" + read -p "Do you want to recreate it? (y/n) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + print_info "Removing existing virtual environment..." + rm -rf "$VENV_DIR" + else + print_info "Using existing virtual environment." + source "$VENV_DIR/bin/activate" || { print_error "Failed to activate virtual environment"; exit 1; } + return 0 + fi + fi + + # Create virtual environment + python3 -m venv "$VENV_DIR" --system-site-packages || { + print_error "Failed to create virtual environment" + print_info "Trying without --system-site-packages..." + python3 -m venv "$VENV_DIR" || { + print_error "Failed to create virtual environment. Please check your Python installation." + exit 1 + } + } + + # Activate virtual environment + source "$VENV_DIR/bin/activate" || { print_error "Failed to activate virtual environment"; exit 1; } + + # Update pip and setuptools + print_info "Updating pip, setuptools, and wheel..." + pip install --upgrade pip setuptools wheel || { print_error "Failed to update pip, setuptools, and wheel"; exit 1; } + + print_info "Virtual environment activated successfully." +} -# Update pip and setuptools -pip install --upgrade pip setuptools wheel +# Check Python version +if ! check_python_version; then + print_warning "Continuing with unsupported Python version. Some features may not work correctly." +fi -print_info "Virtual environment activated." +# Set up virtual environment +setup_virtual_environment # Create activation script for users cat > activate-vocalinux.sh << EOF @@ -145,70 +502,336 @@ EOF chmod +x activate-vocalinux.sh print_info "Created activation script: activate-vocalinux.sh" -# Install Python package -if [[ "$DEV_MODE" == "yes" ]]; then - print_info "Installing Vocalinux in development mode..." - pip install -e . - pip install pytest pytest-mock pytest-cov - # Install all optional dependencies for development - print_info "Installing all optional dependencies for development..." - pip install ".[whisper,dev]" -else - print_info "Installing Vocalinux..." - pip install . +# Function to install Python package with error handling and verification +install_python_package() { + # Create a temporary directory for pip logs + local PIP_LOG_DIR=$(mktemp -d) + local PIP_LOG_FILE="$PIP_LOG_DIR/pip_log.txt" - # Prompt for Whisper installation - read -p "Do you want to install Whisper AI support? This requires additional disk space. (y/n) " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - print_info "Installing Whisper support (this might take a while)..." - pip install ".[whisper]" + # Function to verify package installation + verify_package_installed() { + local PKG_NAME="vocalinux" + python -c "import $PKG_NAME" 2>/dev/null + return $? + } + + if [[ "$DEV_MODE" == "yes" ]]; then + print_info "Installing Vocalinux in development mode..." + + # Install in development mode with logging + pip install -e . --log "$PIP_LOG_FILE" || { + print_error "Failed to install Vocalinux in development mode." + print_error "Check the pip log for details: $PIP_LOG_FILE" + return 1 + } + + # Install test dependencies + print_info "Installing test dependencies..." + pip install pytest pytest-mock pytest-cov --log "$PIP_LOG_FILE" || { + print_warning "Failed to install some test dependencies. Tests may not run correctly." + } + + # Install all optional dependencies for development + print_info "Installing all optional dependencies for development..." + pip install ".[whisper,dev]" --log "$PIP_LOG_FILE" || { + print_warning "Failed to install some optional dependencies." + print_warning "Some features may not work correctly." + } + else + print_info "Installing Vocalinux..." + + # Install the package with logging + pip install . --log "$PIP_LOG_FILE" || { + print_error "Failed to install Vocalinux." + print_error "Check the pip log for details: $PIP_LOG_FILE" + return 1 + } + + # Prompt for Whisper installation + read -p "Do you want to install Whisper AI support? This requires additional disk space. (y/n) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + print_info "Installing Whisper support (this might take a while)..." + pip install ".[whisper]" --log "$PIP_LOG_FILE" || { + print_warning "Failed to install Whisper support." + print_warning "Voice recognition will fall back to VOSK." + } + fi fi + + # Verify installation + if verify_package_installed; then + print_success "Vocalinux package installed successfully!" + # Clean up log file if installation was successful + rm -rf "$PIP_LOG_DIR" + return 0 + else + print_error "Vocalinux package installation verification failed." + print_error "Check the pip log for details: $PIP_LOG_FILE" + return 1 + fi +} + +# Install Python package +if ! install_python_package; then + print_error "Failed to install Vocalinux package. Installation cannot continue." + exit 1 fi +# Function to install desktop entry with error handling +install_desktop_entry() { + print_info "Installing desktop entry..." + + # Check if desktop entry file exists + if [ ! -f "vocalinux.desktop" ]; then + print_error "Desktop entry file not found: vocalinux.desktop" + return 1 + fi + + # Create desktop directory if it doesn't exist + mkdir -p "$DESKTOP_DIR" || { + print_error "Failed to create desktop directory: $DESKTOP_DIR" + return 1 + } + + # Copy desktop entry + cp vocalinux.desktop "$DESKTOP_DIR/" || { + print_error "Failed to copy desktop entry to $DESKTOP_DIR" + return 1 + } + + # Update the desktop entry to use the venv + VENV_SCRIPT_PATH="$(realpath $VENV_DIR/bin/vocalinux)" + if [ ! -f "$VENV_SCRIPT_PATH" ]; then + print_warning "Vocalinux script not found at $VENV_SCRIPT_PATH" + print_warning "Desktop entry may not work correctly" + else + sed -i "s|^Exec=vocalinux|Exec=$VENV_SCRIPT_PATH|" "$DESKTOP_DIR/vocalinux.desktop" || { + print_warning "Failed to update desktop entry path" + } + print_info "Updated desktop entry to use virtual environment" + fi + + # Make desktop entry executable + chmod +x "$DESKTOP_DIR/vocalinux.desktop" || { + print_warning "Failed to make desktop entry executable" + } + + return 0 +} + +# Function to install icons with error handling +install_icons() { + print_info "Installing application icons..." + + # Create icon directory if it doesn't exist + mkdir -p "$ICON_DIR" || { + print_error "Failed to create icon directory: $ICON_DIR" + return 1 + } + + # Check if icons directory exists + if [ ! -d "resources/icons/scalable" ]; then + print_warning "Custom icons not found in resources/icons/scalable directory" + return 1 + fi + + # List of icons to install + local ICONS=( + "vocalinux.svg" + "vocalinux-microphone.svg" + "vocalinux-microphone-off.svg" + "vocalinux-microphone-process.svg" + ) + + # Install each icon + local INSTALLED_COUNT=0 + for icon in "${ICONS[@]}"; do + if [ -f "resources/icons/scalable/$icon" ]; then + cp "resources/icons/scalable/$icon" "$ICON_DIR/" || { + print_warning "Failed to copy icon: $icon" + continue + } + ((INSTALLED_COUNT++)) + else + print_warning "Icon not found: resources/icons/scalable/$icon" + fi + done + + if [ "$INSTALLED_COUNT" -eq "${#ICONS[@]}" ]; then + print_success "Installed all custom Vocalinux icons" + return 0 + elif [ "$INSTALLED_COUNT" -gt 0 ]; then + print_warning "Installed $INSTALLED_COUNT/${#ICONS[@]} custom Vocalinux icons" + return 0 + else + print_error "Failed to install any icons" + return 1 + fi +} + +# Function to update icon cache +update_icon_cache() { + print_info "Updating icon cache..." + + # Check if gtk-update-icon-cache command exists + if command_exists gtk-update-icon-cache; then + gtk-update-icon-cache -f -t "${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" 2>/dev/null || { + print_warning "Failed to update icon cache" + } + else + print_warning "gtk-update-icon-cache command not found, skipping icon cache update" + fi +} + # Install desktop entry -print_info "Installing desktop entry..." -cp vocalinux.desktop "$DESKTOP_DIR/" - -# Update the desktop entry to use the venv -VENV_SCRIPT_PATH="$(realpath $VENV_DIR/bin/vocalinux)" -sed -i "s|^Exec=vocalinux|Exec=$VENV_SCRIPT_PATH|" "$DESKTOP_DIR/vocalinux.desktop" -print_info "Updated desktop entry to use virtual environment" +install_desktop_entry || print_warning "Desktop entry installation failed" # Install icons -print_info "Installing application icons..." -if [ -d "resources/icons/scalable" ]; then - cp resources/icons/scalable/vocalinux.svg "$ICON_DIR/" - cp resources/icons/scalable/vocalinux-microphone.svg "$ICON_DIR/" - cp resources/icons/scalable/vocalinux-microphone-off.svg "$ICON_DIR/" - cp resources/icons/scalable/vocalinux-microphone-process.svg "$ICON_DIR/" - print_info "Installed custom Vocalinux icons" -else - print_warning "Custom icons not found in resources/icons/scalable directory" -fi +install_icons || print_warning "Icon installation failed" -# Update icon cache to make the icons available immediately -gtk-update-icon-cache -f -t "${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" 2>/dev/null || true +# Update icon cache +update_icon_cache + +# Function to run tests with better error handling +run_tests() { + print_info "Running tests..." + + # Check if pytest is installed + if ! python -c "import pytest" &>/dev/null; then + print_info "Installing pytest and related packages..." + pip install pytest pytest-mock pytest-cov || { + print_error "Failed to install pytest. Cannot run tests." + return 1 + } + fi + + # Create a directory for test results + local TEST_RESULTS_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/vocalinux/test_results" + mkdir -p "$TEST_RESULTS_DIR" + local TEST_RESULTS_FILE="$TEST_RESULTS_DIR/pytest_$(date +%Y%m%d_%H%M%S).xml" + + print_info "Running tests with pytest..." + print_info "This may take a few minutes..." + + # Run the tests with pytest and capture output + local TEST_OUTPUT_FILE=$(mktemp) + if pytest -v --junitxml="$TEST_RESULTS_FILE" | tee "$TEST_OUTPUT_FILE"; then + print_success "All tests passed!" + print_info "Test results saved to: $TEST_RESULTS_FILE" + rm -f "$TEST_OUTPUT_FILE" + return 0 + else + local FAILED_COUNT=$(grep -c "FAILED" "$TEST_OUTPUT_FILE") + print_error "$FAILED_COUNT tests failed!" + print_info "Test results saved to: $TEST_RESULTS_FILE" + print_info "Check the test output for details." + rm -f "$TEST_OUTPUT_FILE" + return 1 + fi +} # Run tests if requested if [[ "$RUN_TESTS" == "yes" ]]; then - print_info "Running tests..." - # Install pytest if not already installed - pip install pytest pytest-mock pytest-cov - - # Run the tests with pytest - pytest - if [ $? -eq 0 ]; then - print_success "All tests passed!" + if run_tests; then + print_success "Test suite completed successfully." else - print_error "Some tests failed!" + print_warning "Test suite completed with failures." + print_warning "You can still use the application, but some features might not work as expected." fi fi -print_success "Installation complete!" -print_info "To activate the virtual environment in the future, run:" -print_info " source activate-vocalinux.sh" -print_info "You can then launch Vocalinux by running 'vocalinux'" +# Function to verify the installation +verify_installation() { + print_info "Verifying installation..." + local ISSUES=0 + + # Check if virtual environment exists and is activated + if [ ! -d "$VENV_DIR" ] || [ ! -f "$VENV_DIR/bin/activate" ]; then + print_error "Virtual environment not found or incomplete." + ((ISSUES++)) + fi + + # Check if vocalinux command is available + if ! command -v vocalinux &>/dev/null && [ ! -f "$VENV_DIR/bin/vocalinux" ]; then + print_error "Vocalinux command not found." + ((ISSUES++)) + fi + + # Check if desktop entry is installed + if [ ! -f "$DESKTOP_DIR/vocalinux.desktop" ]; then + print_warning "Desktop entry not found. Application may not appear in application menu." + ((ISSUES++)) + fi + + # Check if icons are installed + local ICON_COUNT=0 + for icon in vocalinux.svg vocalinux-microphone.svg vocalinux-microphone-off.svg vocalinux-microphone-process.svg; do + if [ -f "$ICON_DIR/$icon" ]; then + ((ICON_COUNT++)) + fi + done + + if [ "$ICON_COUNT" -lt 4 ]; then + print_warning "Some icons are missing. Application may not display correctly." + ((ISSUES++)) + fi + + # Check if Python package is importable + if ! python -c "import vocalinux" &>/dev/null; then + print_error "Vocalinux Python package cannot be imported." + ((ISSUES++)) + fi + + # Return the number of issues found + return $ISSUES +} -echo -print_info "For more information, see the documentation in the docs/ directory." +# Function to print installation summary +print_installation_summary() { + local ISSUES=$1 + + echo + echo "==============================" + echo " INSTALLATION SUMMARY" + echo "==============================" + echo + + if [ "$ISSUES" -eq 0 ]; then + print_success "Installation completed successfully with no issues!" + else + print_warning "Installation completed with $ISSUES potential issue(s)." + print_warning "The application may still work, but some features might be limited." + fi + + echo + print_info "Installation details:" + print_info "- Virtual environment: $VENV_DIR" + print_info "- Desktop entry: $DESKTOP_DIR/vocalinux.desktop" + print_info "- Icons: $ICON_DIR" + print_info "- Configuration: $CONFIG_DIR" + print_info "- Data directory: $DATA_DIR" + + echo + print_info "To activate the virtual environment in the future, run:" + print_info " source activate-vocalinux.sh" + print_info "You can then launch Vocalinux by running 'vocalinux'" + + echo + print_info "For more information, see the documentation in the docs/ directory." + + if [ "$ISSUES" -gt 0 ]; then + echo + print_warning "If you encounter any problems, please report them at:" + print_warning "https://github.com/jatinkrmalik/vocalinux/issues" + fi +} + +# Verify the installation +verify_installation +INSTALL_ISSUES=$? + +# Print installation summary +print_installation_summary $INSTALL_ISSUES + +print_success "Installation process completed!" diff --git a/setup.py b/setup.py index cf903f4..8e01922 100644 --- a/setup.py +++ b/setup.py @@ -1,46 +1,94 @@ #!/usr/bin/env python3 -import os +""" +Vocalinux - A seamless voice dictation system for Linux +""" +import os +import sys +import platform from setuptools import find_packages, setup +# Check Python version +MIN_PYTHON_VERSION = (3, 8) +if sys.version_info < MIN_PYTHON_VERSION: + sys.exit(f"Error: Vocalinux requires Python {MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]} or higher") + # This package requires several system dependencies that must be installed manually: # For PyAudio: portaudio19-dev (on Ubuntu/Debian) # For PyGObject: libgirepository1.0-dev, libcairo2-dev, pkg-config, python3-dev +# Get long description from README if available +long_description = "" +if os.path.exists("README.md"): + with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +# Get version from version file or default to 0.1.0 +version = "0.1.0" +if os.path.exists("src/vocalinux/version.py"): + with open("src/vocalinux/version.py", "r", encoding="utf-8") as f: + exec(f.read()) + version = locals().get("__version__", "0.1.0") + +# Platform-specific dependencies +platform_dependencies = [] +if platform.system() == "Linux": + platform_dependencies.extend([ + "python-xlib", # For keyboard shortcut handling in X11 + "PyGObject", # For GTK UI + ]) + setup( name="vocalinux", - version="0.1.0", + version=version, description="A seamless voice dictation system for Linux", - author="@jatinkrmaik", + long_description=long_description, + long_description_content_type="text/markdown", + author="Jatin K Malik", author_email="jatinkrmalik@gmail.com", + url="https://github.com/jatinkrmalik/vocalinux", + project_urls={ + "Bug Tracker": "https://github.com/jatinkrmalik/vocalinux/issues", + "Documentation": "https://github.com/jatinkrmalik/vocalinux/tree/main/docs", + "Source Code": "https://github.com/jatinkrmalik/vocalinux", + }, packages=find_packages(where="src"), package_dir={"": "src"}, install_requires=[ - "vosk", # For VOSK API speech recognition - "pydub", # For audio processing - "python-xlib", # For keyboard shortcut handling in X11 - "pynput", # For keyboard/mouse events - "PyGObject", # For GTK UI - "requests", # For downloading models - "tqdm", # For progress bars during downloads - "numpy", # For numerical operations - "pyaudio", # For audio input/output - ], + "vosk>=0.3.45", # For VOSK API speech recognition + "pydub>=0.25.1", # For audio processing + "pynput>=1.7.6", # For keyboard/mouse events + "requests>=2.28.0", # For downloading models + "tqdm>=4.64.0", # For progress bars during downloads + "numpy>=1.22.0", # For numerical operations + "pyaudio>=0.2.13", # For audio input/output + ] + platform_dependencies, extras_require={ - "whisper": ["whisper", "torch"], # Optional Whisper AI support + "whisper": [ + "whisper>=1.1.10", + "torch>=2.0.0", + ], # Optional Whisper AI support "dev": [ - "pytest", - "pytest-cov", # Add this for coverage testing - "black", - "isort", - "flake8", - "pre-commit", # For pre-commit hooks + "pytest>=7.0.0", + "pytest-cov>=4.0.0", # For coverage testing + "pytest-mock>=3.10.0", # For mocking in tests + "black>=23.0.0", # For code formatting + "isort>=5.12.0", # For import sorting + "flake8>=6.0.0", # For linting + "pre-commit>=3.0.0", # For pre-commit hooks + ], + "docs": [ + "sphinx>=6.0.0", + "sphinx-rtd-theme>=1.2.0", ], }, entry_points={ "console_scripts": [ "vocalinux=vocalinux.main:main", ], + "gui_scripts": [ + "vocalinux-gui=vocalinux.main:main", + ], }, # Include custom application icons and sounds in the package data_files=[ @@ -83,7 +131,15 @@ setup( "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Desktop Environment :: Gnome", + "Topic :: Multimedia :: Sound/Audio :: Speech", + "Topic :: Text Processing", + "Topic :: Utilities", ], - python_requires=">=3.8", # Updated minimum Python version + python_requires=">=3.8", # Minimum Python version + zip_safe=False, # Required for PyGObject applications ) diff --git a/src/vocalinux/version.py b/src/vocalinux/version.py new file mode 100644 index 0000000..22ef249 --- /dev/null +++ b/src/vocalinux/version.py @@ -0,0 +1,9 @@ +""" +Version information for Vocalinux. +""" + +__version__ = "0.1.0" +__author__ = "Jatin K Malik" +__email__ = "jatinkrmalik@gmail.com" +__license__ = "GPL-3.0" +__copyright__ = "Copyright 2023-2024 Jatin K Malik" \ No newline at end of file diff --git a/uninstall.sh b/uninstall.sh index e13a53a..305ecd6 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -2,7 +2,8 @@ # Vocalinux Uninstaller # This script removes Vocalinux and cleans up the environment -set -e # Exit on error +# Don't exit on error to allow for more robust cleanup +# set -e # Function to display colored output print_info() { @@ -21,89 +22,283 @@ print_warning() { echo -e "\e[1;33m[WARNING]\e[0m $1" } +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Function to safely remove a file or directory +safe_remove() { + local path="$1" + local description="$2" + + if [ -e "$path" ]; then + print_info "Removing $description: $path" + rm -rf "$path" && { + print_success "Successfully removed $description" + return 0 + } || { + print_error "Failed to remove $description: $path" + return 1 + } + else + print_info "$description not found: $path" + return 0 + fi +} + print_info "Vocalinux Uninstaller" print_info "==============================" echo "" +# Parse command line arguments +KEEP_CONFIG="no" +KEEP_DATA="no" +VENV_DIR="venv" + +while [[ $# -gt 0 ]]; do + case $1 in + --keep-config) + KEEP_CONFIG="yes" + shift + ;; + --keep-data) + KEEP_DATA="yes" + shift + ;; + --venv-dir=*) + VENV_DIR="${1#*=}" + shift + ;; + --help) + echo "Vocalinux Uninstaller" + echo "Usage: $0 [options]" + echo "Options:" + echo " --keep-config Keep configuration files" + echo " --keep-data Keep application data (models, etc.)" + echo " --venv-dir=PATH Specify custom virtual environment directory (default: venv)" + echo " --help Show this help message" + exit 0 + ;; + *) + print_error "Unknown option: $1" + echo "Use --help to see available options" + exit 1 + ;; + esac +done + # Define XDG directories CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/vocalinux" DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/vocalinux" DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications" ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/scalable/apps" +# Print uninstallation options +echo "Uninstallation options:" +echo "- Virtual environment directory: $VENV_DIR" +[[ "$KEEP_CONFIG" == "yes" ]] && echo "- Keeping configuration files" +[[ "$KEEP_DATA" == "yes" ]] && echo "- Keeping application data" +echo + # Ask for confirmation -read -p "This will completely remove Vocalinux and all its data. Continue? (y/n) " -n 1 -r +read -p "This will remove Vocalinux from your system. Continue? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then print_info "Uninstallation cancelled." exit 0 fi -# Remove virtual environment (if exists) -if [ -d "venv" ]; then - print_info "Removing virtual environment..." - # Deactivate the virtual environment if it's active - if [[ "$VIRTUAL_ENV" == *"venv"* ]]; then - print_warning "Virtual environment is active. Deactivating..." - deactivate +# Function to handle virtual environment removal +remove_virtual_environment() { + if [ -d "$VENV_DIR" ]; then + print_info "Removing virtual environment..." + + # Deactivate the virtual environment if it's active + if [[ -n "$VIRTUAL_ENV" && "$VIRTUAL_ENV" == *"$VENV_DIR"* ]]; then + print_warning "Virtual environment is active. Deactivating..." + deactivate 2>/dev/null || { + print_warning "Failed to deactivate virtual environment. This is not critical." + } + fi + + # Remove the virtual environment directory + safe_remove "$VENV_DIR" "virtual environment directory" + else + print_info "Virtual environment not found: $VENV_DIR" fi - # Remove the virtual environment directory - print_info "Deleting virtual environment directory..." - rm -rf venv -fi +} -# Remove activation script -if [ -f "activate-vocalinux.sh" ]; then - print_info "Removing activation script..." - rm activate-vocalinux.sh -fi +# Function to remove application files +remove_application_files() { + # Remove activation script + safe_remove "activate-vocalinux.sh" "activation script" + + # Remove desktop entry + safe_remove "$DESKTOP_DIR/vocalinux.desktop" "desktop entry" + + # Remove icons + print_info "Removing application icons..." + local ICONS=( + "vocalinux.svg" + "vocalinux-microphone.svg" + "vocalinux-microphone-off.svg" + "vocalinux-microphone-process.svg" + ) + + for icon in "${ICONS[@]}"; do + safe_remove "$ICON_DIR/$icon" "icon" + done + + # Update icon cache + if command_exists gtk-update-icon-cache; then + print_info "Updating icon cache..." + gtk-update-icon-cache -f -t "${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" 2>/dev/null || { + print_warning "Failed to update icon cache. This is not critical." + } + fi +} -# Remove application configuration and data -if [ -d "$CONFIG_DIR" ]; then - print_info "Removing configuration directory..." - rm -rf "$CONFIG_DIR" -fi +# Function to remove configuration and data +remove_config_and_data() { + # Remove configuration if not keeping it + if [[ "$KEEP_CONFIG" != "yes" ]]; then + safe_remove "$CONFIG_DIR" "configuration directory" + else + print_info "Keeping configuration directory as requested: $CONFIG_DIR" + fi + + # Remove data if not keeping it + if [[ "$KEEP_DATA" != "yes" ]]; then + safe_remove "$DATA_DIR" "application data directory" + else + print_info "Keeping application data directory as requested: $DATA_DIR" + fi +} -if [ -d "$DATA_DIR" ]; then - print_info "Removing application data directory..." - rm -rf "$DATA_DIR" -fi +# Function to clean up build artifacts +cleanup_build_artifacts() { + print_info "Cleaning up build artifacts..." + + # Remove Python package build directories + safe_remove "build/" "build directory" + safe_remove "dist/" "distribution directory" + safe_remove "*.egg-info/" "egg-info directory" + + # Find and remove egg-info directories in src + find src -name "*.egg-info" -type d -exec rm -rf {} \; 2>/dev/null || { + print_warning "Failed to remove some egg-info directories." + } + + # Find and remove __pycache__ directories + find . -name "__pycache__" -type d -exec rm -rf {} \; 2>/dev/null || { + print_warning "Failed to remove some __pycache__ directories." + } + + # Clean up any temporary or generated files + print_info "Cleaning up temporary files..." + find . -name "*.pyc" -delete 2>/dev/null + find . -name "*.pyo" -delete 2>/dev/null + find . -name ".pytest_cache" -type d -exec rm -rf {} \; 2>/dev/null + find . -name ".coverage" -delete 2>/dev/null + + # Remove wrapper script if it exists + safe_remove "vocalinux-run.py" "wrapper script" +} -# Remove desktop entry -if [ -f "$DESKTOP_DIR/vocalinux.desktop" ]; then - print_info "Removing desktop entry..." - rm "$DESKTOP_DIR/vocalinux.desktop" -fi +# Function to verify uninstallation +verify_uninstallation() { + print_info "Verifying uninstallation..." + local ISSUES=0 + + # Check if virtual environment still exists + if [ -d "$VENV_DIR" ]; then + print_warning "Virtual environment still exists: $VENV_DIR" + ((ISSUES++)) + fi + + # Check if desktop entry still exists + if [ -f "$DESKTOP_DIR/vocalinux.desktop" ]; then + print_warning "Desktop entry still exists: $DESKTOP_DIR/vocalinux.desktop" + ((ISSUES++)) + fi + + # Check if any icons still exist + local ICON_COUNT=0 + for icon in vocalinux.svg vocalinux-microphone.svg vocalinux-microphone-off.svg vocalinux-microphone-process.svg; do + if [ -f "$ICON_DIR/$icon" ]; then + ((ICON_COUNT++)) + fi + done + + if [ "$ICON_COUNT" -gt 0 ]; then + print_warning "$ICON_COUNT icons still exist in $ICON_DIR" + ((ISSUES++)) + fi + + # Check if configuration still exists (only if not keeping it) + if [[ "$KEEP_CONFIG" != "yes" && -d "$CONFIG_DIR" ]]; then + print_warning "Configuration directory still exists: $CONFIG_DIR" + ((ISSUES++)) + fi + + # Check if data still exists (only if not keeping it) + if [[ "$KEEP_DATA" != "yes" && -d "$DATA_DIR" ]]; then + print_warning "Application data directory still exists: $DATA_DIR" + ((ISSUES++)) + fi + + # Return the number of issues found + return $ISSUES +} -# Remove icons -print_info "Removing application icons..." -rm -f "$ICON_DIR/vocalinux.svg" -rm -f "$ICON_DIR/vocalinux-microphone.svg" -rm -f "$ICON_DIR/vocalinux-microphone-off.svg" -rm -f "$ICON_DIR/vocalinux-microphone-process.svg" +# Function to print uninstallation summary +print_uninstallation_summary() { + local ISSUES=$1 + + echo + echo "==============================" + echo " UNINSTALLATION SUMMARY" + echo "==============================" + echo + + if [ "$ISSUES" -eq 0 ]; then + print_success "Uninstallation completed successfully with no issues!" + else + print_warning "Uninstallation completed with $ISSUES potential issue(s)." + print_warning "Some files or directories may still exist." + fi + + echo + if [[ "$KEEP_CONFIG" == "yes" ]]; then + print_info "Configuration directory was kept: $CONFIG_DIR" + fi + + if [[ "$KEEP_DATA" == "yes" ]]; then + print_info "Application data directory was kept: $DATA_DIR" + fi + + echo + print_info "Your system has been cleaned up and is ready for a fresh installation." + + if [ "$ISSUES" -gt 0 ]; then + echo + print_warning "If you encounter any problems, please report them at:" + print_warning "https://github.com/jatinkrmalik/vocalinux/issues" + fi +} -# Update icon cache -gtk-update-icon-cache -f -t "${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" 2>/dev/null || true +# Perform uninstallation steps +remove_virtual_environment +remove_application_files +remove_config_and_data +cleanup_build_artifacts -# Remove Python package build directories -print_info "Removing build directories..." -rm -rf build/ dist/ *.egg-info/ -find src -name "*.egg-info" -type d -exec rm -rf {} + -find src -name "__pycache__" -type d -exec rm -rf {} + +# Verify uninstallation +verify_uninstallation +UNINSTALL_ISSUES=$? -# Clean up any temporary or generated files -print_info "Cleaning up temporary files..." -find . -name "*.pyc" -delete -find . -name "*.pyo" -delete -find . -name "__pycache__" -type d -exec rm -rf {} + -find . -name ".pytest_cache" -type d -exec rm -rf {} + -find . -name ".coverage" -delete +# Print uninstallation summary +print_uninstallation_summary $UNINSTALL_ISSUES -# Remove wrapper script if it exists -if [ -f "vocalinux-run.py" ]; then - print_info "Removing wrapper script..." - rm vocalinux-run.py -fi - -print_success "Uninstallation complete!" -print_info "Your system has been cleaned up and is ready for a fresh installation." +print_success "Uninstallation process completed!"