diff --git a/.github/labeler.yml b/.github/labeler.yml index e38da32..ddce3ad 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -29,7 +29,7 @@ ci: - changed-files: - any-glob-to-any-file: - ".github/**/*" - - "Makefile" + - "justfile" - "scripts/**/*" - ".flake8" - ".pre-commit-config.yaml" diff --git a/.github/workflows/unified-pipeline.yml b/.github/workflows/unified-pipeline.yml index 975310d..af44814 100644 --- a/.github/workflows/unified-pipeline.yml +++ b/.github/workflows/unified-pipeline.yml @@ -38,7 +38,7 @@ jobs: - 'tests/**' - 'pyproject.toml' - 'requirements*.txt' - - 'Makefile' + - 'justfile' - '.pre-commit-config.yaml' - '.github/workflows/unified-pipeline.yml' - name: Check if website should deploy diff --git a/AGENTS.md b/AGENTS.md index 18e0656..bd68e69 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,20 +47,20 @@ pytest -m "not slow" pytest -m "not integration" # Lint (check only) -make lint +just lint # Or manually: flake8 src/ tests/ --select=E9,F63,F7,F82 black --check --diff src/ tests/ isort --check-only --diff --profile black src/ tests/ # Auto-format code -make format +just format # Or manually: black src/ tests/ isort --profile black src/ tests/ # Type checking -make typecheck +just typecheck # Or: mypy src/ # Run the application @@ -194,7 +194,7 @@ Quick summary: 1. Update version in `src/vocalinux/version.py` 2. Update version references in README.md, docs/INSTALL.md, docs/UPDATE.md 3. Update web/src/app/page.tsx and web/package.json -4. Run `make lint` to verify code quality +4. Run `just lint` to verify code quality 5. Create branch `release/vX.Y.Z-PHASE` 6. Commit with `chore(release): prepare vX.Y.Z-PHASE` 7. Push and create PR @@ -242,7 +242,7 @@ docs(readme): update installation instructions The startup update script keeps a Python venv (`venv/`) and `web/node_modules` in sync. Standard commands live in the sections above and in `web/AGENTS.md`; notes below are the non-obvious gotchas for this environment. -- **Activate the venv first.** Python tooling (`vocalinux`, `pytest`, `make lint`, `mypy`) lives in `venv/`. Run `source venv/bin/activate` (or prefix with `./venv/bin/`) before use. +- **Activate the venv first.** Python tooling (`vocalinux`, `pytest`, `just lint`, `mypy`) lives in `venv/`. Run `source venv/bin/activate` (or prefix with `./venv/bin/`) before use. - **The venv must be created with `--system-site-packages`.** GTK/`PyGObject` come from the apt package `python3-gi`; installing `PyGObject` from pip fails on Ubuntu 24.04 because the pinned version needs `girepository-2.0` (glib 2.80+), which the distro doesn't ship. The update script already creates the venv this way — don't drop that flag. - **`black --check` prints a Python-version warning.** `pyproject.toml` targets py314 but the VM runs Python 3.12; Black still reports "All done" and lint passes. This warning is benign. - **Desktop app is a GTK tray app.** An XFCE session (`xfwm4` + `xfce4-panel`) runs on `DISPLAY=:1`. Always give the app the session env: `DISPLAY=:1`, `DBUS_SESSION_BUS_ADDRESS=autolaunch:`, `XDG_RUNTIME_DIR=/run/user/1000`, `XDG_CURRENT_DESKTOP=XFCE`. Single-instance lock lives at `~/.local/share/vocalinux/instance.lock`; delete it after killing a stale instance. Kill instances by explicit PID (never `pkill -f`). diff --git a/Makefile b/Makefile deleted file mode 100644 index 27cc02e..0000000 --- a/Makefile +++ /dev/null @@ -1,105 +0,0 @@ -# Vocalinux Makefile -# Convenient commands for development - -.PHONY: help install install-dev test lint format clean build release - -# Default target -help: - @echo "Vocalinux Development Commands" - @echo "==============================" - @echo "" - @echo "Setup:" - @echo " make install - Install Vocalinux" - @echo " make install-dev - Install in development mode" - @echo "" - @echo "Development:" - @echo " make test - Run test suite" - @echo " make lint - Run linters (flake8, black, isort)" - @echo " make format - Auto-format code" - @echo " make typecheck - Run type checking (mypy)" - @echo "" - @echo "Build:" - @echo " make build - Build distribution packages" - @echo " make clean - Remove build artifacts" - @echo "" - @echo "Run:" - @echo " make run - Run the application" - @echo " make run-debug - Run with debug logging" - -# Installation -install: - ./install.sh - -install-dev: - ./install.sh --dev - -# Testing -test: - @echo "Running tests..." - pytest -v - -test-cov: - @echo "Running tests with coverage..." - pytest --cov=src --cov-report=html --cov-report=term - @echo "Coverage report generated in htmlcov/" - -# Linting -lint: - @echo "Running flake8..." - flake8 src/ tests/ --count --select=E9,F63,F7,F82 --show-source --statistics - @echo "Checking black formatting..." - black --check --diff src/ tests/ - @echo "Checking isort..." - isort --check-only --diff --profile black src/ tests/ - -format: - @echo "Formatting with black..." - black src/ tests/ - @echo "Sorting imports with isort..." - isort --profile black src/ tests/ - -typecheck: - @echo "Running mypy..." - mypy src/ - -# Build -build: - @echo "Building distribution packages..." - python -m build - @echo "Built packages in dist/" - -clean: - @echo "Cleaning build artifacts..." - rm -rf build/ - rm -rf dist/ - rm -rf *.egg-info - rm -rf src/*.egg-info - find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true - find . -type f -name "*.pyc" -delete - find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true - find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true - rm -rf htmlcov/ - rm -f .coverage - rm -f coverage.xml - @echo "Clean complete" - -# Run -run: - vocalinux - -run-debug: - vocalinux --debug - -run-source: - python -m vocalinux.main - -run-source-debug: - python -m vocalinux.main --debug - -# Pre-commit -pre-commit: - pre-commit run --all-files - -# Version info -version: - @python -c "from src.vocalinux.version import __version__; print(__version__)" diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index c900485..bddfa4c 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -37,10 +37,10 @@ Use this checklist for every release: - [ ] Confirm install docs/website mention **AppImage** when this release ships AppImages ### Testing & Verification -- [ ] Run `make test` - All tests pass -- [ ] Run `make lint` - No linting errors -- [ ] Run `make typecheck` - No type errors -- [ ] Run `make build` - Package builds successfully +- [ ] Run `just test` - All tests pass +- [ ] Run `just lint` - No linting errors +- [ ] Run `just typecheck` - No type errors +- [ ] Run `just build` - Package builds successfully - [ ] Run `npm run build` in `web/` - Website builds successfully > Note: CI workflows also sync website version metadata from `src/vocalinux/version.py`, but source files should still be updated in-repo during release prep for review clarity. @@ -262,16 +262,16 @@ Run the full verification suite: ```bash # Run tests -make test +just test # Run linting -make lint +just lint # Run type checking -make typecheck +just typecheck # Build package -make build +just build # Website build cd web && npm run build && cd .. diff --git a/install.sh b/install.sh index df7b333..a51f1ac 100755 --- a/install.sh +++ b/install.sh @@ -2,7 +2,16 @@ # Vocalinux Installer # This script installs the Vocalinux application and its dependencies -set -e # Exit on error +# -E: ERR trap propagates into functions/subshells +# -e: exit on unhandled command failure -u: error on unset variables +# -o pipefail: a pipeline fails if any stage fails +set -Eeuo pipefail + +# Exit codes (see --help). 1 remains the generic/unclassified failure. +EXIT_OK=0 +EXIT_MISSING_DEPS=2 # required system tools/packages could not be installed +EXIT_NETWORK=3 # connectivity failure or download/clone failure +EXIT_USER_ABORT=4 # user declined a prompt # Keep the venv isolated from ~/.local site packages while still allowing # --system-site-packages to expose distro-provided GTK/PyGObject bindings. @@ -29,6 +38,27 @@ command_exists() { command -v "$1" >/dev/null 2>&1 } +# HTTP-level connectivity check. ICMP ping is blocked on many networks +# (corporate firewalls, public Wi-Fi, CI runners), so probe the actual +# endpoints the installer depends on. Returns 0 if any probe succeeds. +check_connectivity() { + local url + for url in https://pypi.org/simple/ https://api.github.com/ https://huggingface.co/; do + if command_exists curl; then + if curl -fsI --connect-timeout 5 --max-time 10 -o /dev/null "$url" 2>/dev/null; then + return 0 + fi + elif command_exists wget; then + if wget -q --spider --timeout=10 "$url" 2>/dev/null; then + return 0 + fi + else + return 1 + fi + done + return 1 +} + is_kde_plasma_session() { local desktop="${XDG_CURRENT_DESKTOP:-} ${DESKTOP_SESSION:-} ${GDMSESSION:-}" local kde_session="${KDE_FULL_SESSION:-}" @@ -162,7 +192,7 @@ check_running_processes() { if [[ $REPLY =~ ^[Nn]$ ]]; then print_error "Cannot proceed with installation while Vocalinux is running." print_info "Please stop Vocalinux manually and run the installer again." - exit 1 + exit "$EXIT_USER_ABORT" fi fi @@ -212,6 +242,13 @@ GPU_NAME="" GPU_MEMORY="" HAS_VULKAN="no" VULKAN_DEVICE="" +# Initialize mode/state variables that are set later by flags or prompts so +# that every read under `set -u` is well-defined in every code path. +INSTALL_TAG="" +SELECTED_ENGINE="" +WHISPERCPP_BACKEND="" +WHISPERCPP_ALREADY_INSTALLED="false" +REMOTE_API_URL="" # Detect if running non-interactively (e.g., via curl | bash) # If stdin is a pipe but /dev/tty exists, redirect stdin so user input works normally. @@ -311,6 +348,16 @@ while [[ $# -gt 0 ]]; do echo " $0 --auto # Auto-install with whisper.cpp" echo " $0 --auto --engine=vosk # Auto-install VOSK only" echo " $0 --dev --test # Dev mode with tests" + echo "" + echo "A full transcript of every run is saved to" + echo " ~/.local/state/vocalinux/install-.log" + echo "" + echo "Exit codes:" + echo " 0 success" + echo " 1 generic failure" + echo " 2 required system dependencies could not be installed" + echo " 3 network / download failure" + echo " 4 user aborted at a prompt" exit 0 ;; *) @@ -326,6 +373,37 @@ if [[ "$DEV_MODE" == "yes" ]]; then RUN_TESTS="yes" fi +# --------------------------------------------------------------------------- +# Global install log: everything (stdout + stderr) is teed to this file so +# failures can be debugged after the fact. The path is printed at exit. +# --------------------------------------------------------------------------- +INSTALL_LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/vocalinux" +if ! mkdir -p "$INSTALL_LOG_DIR" 2>/dev/null; then + INSTALL_LOG_DIR="${TMPDIR:-/tmp}" +fi +INSTALL_LOG_FILE="$INSTALL_LOG_DIR/install-$(date +%Y%m%d-%H%M%S).log" +exec > >(tee -a "$INSTALL_LOG_FILE") 2>&1 + +# Scratch directory for downloads and temporary build files. Removed on +# success; kept for inspection (with a notice) when the install fails. +VOCALINUX_TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/vocalinux-install.XXXXXXXX")" + +cleanup_on_exit() { + local rc=$? + if [ "$rc" -eq "$EXIT_OK" ]; then + rm -rf "$VOCALINUX_TMP_DIR" + else + print_error "" + print_error "Installation did not complete (exit code $rc)." + print_error "Full install log: $INSTALL_LOG_FILE" + print_error "Scratch files kept for inspection: $VOCALINUX_TMP_DIR" + fi +} +trap cleanup_on_exit EXIT +trap 'print_error "Unexpected error near line $LINENO (exit code $?); see $INSTALL_LOG_FILE"' ERR +trap 'print_warning "Interrupted by user"; exit 130' INT +trap 'print_warning "Terminated by signal"; exit 143' TERM + # Display ASCII art banner cat << "EOF" @@ -346,19 +424,25 @@ check_running_processes resolve_install_tag() { if [ -n "$INSTALL_TAG" ]; then - return + return 0 fi if command_exists curl; then local latest - latest=$(curl -fsSL --connect-timeout 5 \ + # (|| true: pipefail would otherwise abort when head closes the pipe + # early or the API is unreachable; empty means "not resolved") + latest=$(curl -fsSL --connect-timeout 5 --retry 2 \ "https://api.github.com/repos/VocaHQ/vocalinux/releases/latest" \ - 2>/dev/null | grep '"tag_name"' | head -1 | cut -d'"' -f4) + 2>/dev/null | grep '"tag_name"' | head -1 | cut -d'"' -f4 || true) if [ -n "$latest" ]; then INSTALL_TAG="$latest" - return + return 0 fi fi - INSTALL_TAG="v0.10.1-beta" + # No hardcoded fallback tag: silently installing a stale release is worse + # than failing. The remote-install path below aborts with a --tag hint if + # INSTALL_TAG is still empty; in-repo installs do not need a tag at all. + print_warning "Could not determine the latest release tag (GitHub API unreachable?)." + return 0 } resolve_install_tag @@ -380,15 +464,15 @@ ensure_git_installed() { local DISTRO_FAMILY="unknown" if [ -f /etc/os-release ]; then . /etc/os-release - if [[ "$ID" == "ubuntu" || "$ID_LIKE" == *"ubuntu"* || "$ID" == "pop" || "$ID" == "linuxmint" || "$ID" == "elementary" || "$ID" == "zorin" ]]; then + 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 + 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 + 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 + elif [[ "$ID" == "arch" || "${ID_LIKE:-}" == *"arch"* || "$ID" == "manjaro" || "$ID" == "endeavouros" ]]; then DISTRO_FAMILY="arch" - elif [[ "$ID" == "opensuse" || "$ID_LIKE" == *"suse"* ]]; then + elif [[ "$ID" == "opensuse" || "${ID_LIKE:-}" == *"suse"* ]]; then DISTRO_FAMILY="suse" elif [[ "$ID" == "gentoo" ]]; then DISTRO_FAMILY="gentoo" @@ -408,68 +492,68 @@ ensure_git_installed() { sudo apt update && sudo apt install -y git || { print_error "Failed to install git. Please install git manually and run the installer again." print_error " Ubuntu/Debian: sudo apt install git" - exit 1 + exit "$EXIT_MISSING_DEPS" } ;; fedora) sudo dnf install -y git || { print_error "Failed to install git. Please install git manually and run the installer again." print_error " Fedora: sudo dnf install git" - exit 1 + exit "$EXIT_MISSING_DEPS" } ;; arch) sudo pacman -S --noconfirm git || { print_error "Failed to install git. Please install git manually and run the installer again." print_error " Arch: sudo pacman -S git" - exit 1 + exit "$EXIT_MISSING_DEPS" } ;; suse) sudo zypper install -y git || { print_error "Failed to install git. Please install git manually and run the installer again." print_error " openSUSE: sudo zypper install git" - exit 1 + exit "$EXIT_MISSING_DEPS" } ;; gentoo) sudo emerge git || { print_error "Failed to install git. Please install git manually and run the installer again." print_error " Gentoo: sudo emerge git" - exit 1 + exit "$EXIT_MISSING_DEPS" } ;; alpine) sudo apk add git || { print_error "Failed to install git. Please install git manually and run the installer again." print_error " Alpine: sudo apk add git" - exit 1 + exit "$EXIT_MISSING_DEPS" } ;; void) sudo xbps-install -Sy git || { print_error "Failed to install git. Please install git manually and run the installer again." print_error " Void: sudo xbps-install -Sy git" - exit 1 + exit "$EXIT_MISSING_DEPS" } ;; solus) sudo eopkg install git || { print_error "Failed to install git. Please install git manually and run the installer again." print_error " Solus: sudo eopkg install git" - exit 1 + exit "$EXIT_MISSING_DEPS" } ;; mageia) if command -v dnf >/dev/null 2>&1; then sudo dnf install -y git || { print_error "Failed to install git. Please install git manually and run the installer again." - exit 1 + exit "$EXIT_MISSING_DEPS" } else sudo urpmi --force git || { print_error "Failed to install git. Please install git manually and run the installer again." - exit 1 + exit "$EXIT_MISSING_DEPS" } fi ;; @@ -480,7 +564,7 @@ ensure_git_installed() { print_error " Fedora/RHEL: sudo dnf install git" print_error " Arch: sudo pacman -S git" print_error " openSUSE: sudo zypper install git" - exit 1 + exit "$EXIT_MISSING_DEPS" ;; esac @@ -508,6 +592,11 @@ if [ "$IS_VOCALINUX_LOCAL" = true ]; then esac else # Running remotely (e.g., via curl | bash) + if [ -z "$INSTALL_TAG" ]; then + print_error "No release tag available: the GitHub API was unreachable and no --tag was given." + print_error "Re-run with an explicit release tag, e.g.: --tag=v0.15.0" + exit "$EXIT_NETWORK" + fi print_info "Installing Vocalinux version: ${INSTALL_TAG}" # Ensure git is installed before attempting to clone @@ -519,13 +608,16 @@ else if [ -d "$INSTALL_DIR/.git" ]; then print_info "Updating existing clone..." cd "$INSTALL_DIR" - git fetch origin tag "$INSTALL_TAG" - git reset --hard "$INSTALL_TAG" + if ! git fetch origin tag "$INSTALL_TAG" || ! git reset --hard "$INSTALL_TAG"; then + print_error "Failed to update the Vocalinux clone to $INSTALL_TAG." + print_error "Check the install log and your network connection: $INSTALL_LOG_FILE" + exit "$EXIT_NETWORK" + fi else rm -rf "$INSTALL_DIR" git clone --depth 1 --branch "$INSTALL_TAG" "$REPO_URL" "$INSTALL_DIR" || { print_error "Failed to clone Vocalinux repository" - exit 1 + exit "$EXIT_NETWORK" } cd "$INSTALL_DIR" fi @@ -554,21 +646,21 @@ fi detect_distro() { if [ -f /etc/os-release ]; then . /etc/os-release - DISTRO_NAME="$NAME" - DISTRO_ID="$ID" - DISTRO_VERSION="$VERSION_ID" + DISTRO_NAME="${NAME:-unknown}" + DISTRO_ID="${ID:-unknown}" + 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 + 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 + 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 + 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 + elif [[ "$ID" == "arch" || "${ID_LIKE:-}" == *"arch"* || "$ID" == "manjaro" || "$ID" == "endeavouros" ]]; then DISTRO_FAMILY="arch" - elif [[ "$ID" == "opensuse" || "$ID_LIKE" == *"suse"* ]]; then + elif [[ "$ID" == "opensuse" || "${ID_LIKE:-}" == *"suse"* ]]; then DISTRO_FAMILY="suse" elif [[ "$ID" == "gentoo" ]]; then DISTRO_FAMILY="gentoo" @@ -609,8 +701,9 @@ detect_nvidia_gpu() { # Check if nvidia-smi command exists and can successfully query GPU if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi >/dev/null 2>&1; then # Extract GPU information for user feedback - GPU_NAME=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -n1) - GPU_MEMORY=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader 2>/dev/null | head -n1) + # (|| true: nvidia-smi may fail mid-pipeline; pipefail must not abort) + GPU_NAME=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -n1 || true) + GPU_MEMORY=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader 2>/dev/null | head -n1 || true) HAS_NVIDIA_GPU="yes" return 0 else @@ -758,11 +851,14 @@ get_cuda_cmake_args() { detect_vulkan() { # Check for vulkaninfo command if command -v vulkaninfo >/dev/null 2>&1; then - local vulkan_output=$(vulkaninfo --summary 2>/dev/null | head -20) + # (|| true guards against pipefail aborts: head closes the pipe early + # and grep exits 1 when there is no match) + local vulkan_output + vulkan_output=$(vulkaninfo --summary 2>/dev/null | head -20 || true) if [ -n "$vulkan_output" ]; then HAS_VULKAN="yes" # Try to extract GPU name - VULKAN_DEVICE=$(echo "$vulkan_output" | grep -i "deviceName" | head -1 | cut -d'=' -f2 | xargs) + VULKAN_DEVICE=$(echo "$vulkan_output" | grep -i "deviceName" | head -1 | cut -d'=' -f2 | xargs || true) if [ -z "$VULKAN_DEVICE" ]; then VULKAN_DEVICE="Vulkan-compatible GPU" fi @@ -817,7 +913,7 @@ check_vulkan_gpu_compatibility() { # Get all device names from vulkaninfo local DEVICE_NAMES_RAW - DEVICE_NAMES_RAW=$(vulkaninfo --summary 2>/dev/null | awk -F'=' '/deviceName/ {gsub(/^[ \t]+|[ \t]+$/, "", $2); if ($2 != "") print $2}') + DEVICE_NAMES_RAW=$(vulkaninfo --summary 2>/dev/null | awk -F'=' '/deviceName/ {gsub(/^[ \t]+|[ \t]+$/, "", $2); if ($2 != "") print $2}' || true) # Separate hardware GPUs from software renderers local HARDWARE_GPUS="" @@ -1368,7 +1464,7 @@ elif [[ "$DISTRO_FAMILY" != "ubuntu" ]]; then read -p "Do you want to continue anyway? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then - exit 1 + exit "$EXIT_USER_ABORT" fi fi else @@ -1380,7 +1476,7 @@ else read -p "Do you want to continue anyway? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then - exit 1 + exit "$EXIT_USER_ABORT" fi fi fi @@ -2006,7 +2102,7 @@ install_system_dependencies() { read -p "Continue anyway? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then - exit 1 + exit "$EXIT_USER_ABORT" fi else print_info "Non-interactive mode: continuing (dependencies may be missing)..." @@ -2050,7 +2146,7 @@ install_text_input_tools() { 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) + SESSION_TYPE=$(loginctl show-session $(loginctl | grep $(whoami) | awk '{print $1}') -p Type | cut -d= -f2 || true) fi print_info "Detected session type: $SESSION_TYPE" @@ -2878,8 +2974,8 @@ install_python_package() { # Detect GI_TYPELIB_PATH early for cross-distro compatibility # This ensures the path is available for both verification and wrapper scripts - local GI_TYPELIB_DETECTED - GI_TYPELIB_DETECTED=$(detect_typelib_path) + # NOTE: global on purpose — install_desktop_entry (top level) reuses it. + GI_TYPELIB_DETECTED=$(detect_typelib_path || true) print_info "Detected GI_TYPELIB_PATH: $GI_TYPELIB_DETECTED" local WHISPERCPP_ALREADY_INSTALLED=false @@ -3360,9 +3456,9 @@ install_whisper_model() { return 1 fi - # Test internet connectivity - if ! ping -c 1 google.com >/dev/null 2>&1; then - print_warning "No internet connection detected." + # Test internet connectivity (HTTP probes; ICMP ping is often blocked) + if ! check_connectivity; then + print_warning "No internet connection detected (HTTP probes to pypi.org/api.github.com/huggingface.co failed)." print_warning "Whisper model will be downloaded on first application run." return 1 fi @@ -3374,13 +3470,14 @@ install_whisper_model() { # Download the model if command -v wget >/dev/null 2>&1; then - if ! wget --progress=bar:force:noscroll -O "$TEMP_FILE" "$TINY_MODEL_URL" 2>&1; then + if ! wget --progress=bar:force:noscroll --tries=3 --timeout=60 -O "$TEMP_FILE" "$TINY_MODEL_URL" 2>&1; then print_error "Failed to download Whisper model with wget" rm -f "$TEMP_FILE" return 1 fi elif command -v curl >/dev/null 2>&1; then - if ! curl -L --progress-bar -o "$TEMP_FILE" "$TINY_MODEL_URL"; then + # -f: fail on HTTP errors instead of saving the error page as the model + if ! curl -fL --progress-bar --retry 3 --retry-delay 2 -o "$TEMP_FILE" "$TINY_MODEL_URL"; then print_error "Failed to download Whisper model with curl" rm -f "$TEMP_FILE" return 1 @@ -3438,9 +3535,9 @@ install_vosk_models() { return 1 fi - # Test internet connectivity - if ! ping -c 1 google.com >/dev/null 2>&1; then - print_warning "No internet connection detected." + # Test internet connectivity (HTTP probes; ICMP ping is often blocked) + if ! check_connectivity; then + print_warning "No internet connection detected (HTTP probes to pypi.org/api.github.com/huggingface.co failed)." print_warning "VOSK models will be downloaded on first application run." return 1 fi @@ -3452,13 +3549,14 @@ install_vosk_models() { # Download the model if command -v wget >/dev/null 2>&1; then - if ! wget --progress=bar:force:noscroll -O "$TEMP_ZIP" "$SMALL_MODEL_URL" 2>&1; then + if ! wget --progress=bar:force:noscroll --tries=3 --timeout=60 -O "$TEMP_ZIP" "$SMALL_MODEL_URL" 2>&1; then print_error "Failed to download VOSK model with wget" rm -f "$TEMP_ZIP" return 1 fi elif command -v curl >/dev/null 2>&1; then - if ! curl -L --progress-bar -o "$TEMP_ZIP" "$SMALL_MODEL_URL"; then + # -f: fail on HTTP errors instead of saving the error page as the model + if ! curl -fL --progress-bar --retry 3 --retry-delay 2 -o "$TEMP_ZIP" "$SMALL_MODEL_URL"; then print_error "Failed to download VOSK model with curl" rm -f "$TEMP_ZIP" return 1 @@ -3532,9 +3630,9 @@ install_whispercpp_model() { return 1 fi - # Test internet connectivity - if ! ping -c 1 google.com >/dev/null 2>&1; then - print_warning "No internet connection detected." + # Test internet connectivity (HTTP probes; ICMP ping is often blocked) + if ! check_connectivity; then + print_warning "No internet connection detected (HTTP probes to pypi.org/api.github.com/huggingface.co failed)." print_warning "whisper.cpp model will be downloaded on first application run." return 1 fi @@ -3546,13 +3644,14 @@ install_whispercpp_model() { # Download the model if command -v wget >/dev/null 2>&1; then - if ! wget --progress=bar:force:noscroll -O "$TEMP_FILE" "$TINY_MODEL_URL" 2>&1; then + if ! wget --progress=bar:force:noscroll --tries=3 --timeout=60 -O "$TEMP_FILE" "$TINY_MODEL_URL" 2>&1; then print_error "Failed to download whisper.cpp model with wget" rm -f "$TEMP_FILE" return 1 fi elif command -v curl >/dev/null 2>&1; then - if ! curl -L --progress-bar -o "$TEMP_FILE" "$TINY_MODEL_URL"; then + # -f: fail on HTTP errors instead of saving the error page as the model + if ! curl -fL --progress-bar --retry 3 --retry-delay 2 -o "$TEMP_FILE" "$TINY_MODEL_URL"; then print_error "Failed to download whisper.cpp model with curl" rm -f "$TEMP_FILE" return 1 @@ -4026,7 +4125,7 @@ EOF echo "" # Installation details (optional, for debugging) - if [[ "$VERBOSE" == "yes" ]]; then + if [[ "${VERBOSE:-no}" == "yes" ]]; then echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " 🔍 Installation Details (Debug Mode)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @@ -4041,8 +4140,10 @@ EOF } # Verify the installation -verify_installation -INSTALL_ISSUES=$? +# (guarded: verify_installation returns the number of issues found, which +# would otherwise abort the script under set -e before the summary prints) +INSTALL_ISSUES=0 +verify_installation || INSTALL_ISSUES=$? # Print welcome message print_welcome_message $INSTALL_ISSUES diff --git a/justfile b/justfile new file mode 100644 index 0000000..81ab40e --- /dev/null +++ b/justfile @@ -0,0 +1,94 @@ +# Vocalinux justfile +# Convenient commands for development +# Run `just` to list all recipes. Python tooling (pytest, black, mypy) lives in +# venv/ — run `source venv/bin/activate` first. + +# List available recipes +default: + @just --list + +# Install Vocalinux +install: + ./install.sh + +# Install in development mode +install-dev: + ./install.sh --dev + +# Run test suite +test: + @echo "Running tests..." + pytest -v + +# Run tests with coverage +test-cov: + @echo "Running tests with coverage..." + pytest --cov=src --cov-report=html --cov-report=term + @echo "Coverage report generated in htmlcov/" + +# Run linters (flake8, black, isort) +lint: + @echo "Running flake8..." + flake8 src/ tests/ --count --select=E9,F63,F7,F82 --show-source --statistics + @echo "Checking black formatting..." + black --check --diff src/ tests/ + @echo "Checking isort..." + isort --check-only --diff --profile black src/ tests/ + +# Auto-format code (black + isort) +format: + @echo "Formatting with black..." + black src/ tests/ + @echo "Sorting imports with isort..." + isort --profile black src/ tests/ + +# Run type checking (mypy) +typecheck: + @echo "Running mypy..." + mypy src/ + +# Build distribution packages +build: + @echo "Building distribution packages..." + python -m build + @echo "Built packages in dist/" + +# Remove build artifacts +clean: + @echo "Cleaning build artifacts..." + rm -rf build/ + rm -rf dist/ + rm -rf *.egg-info + rm -rf src/*.egg-info + find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + find . -type f -name "*.pyc" -delete + find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true + find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true + rm -rf htmlcov/ + rm -f .coverage + rm -f coverage.xml + @echo "Clean complete" + +# Run the installed application +run: + vocalinux + +# Run the installed application with debug logging +run-debug: + vocalinux --debug + +# Run from source +run-source: + python -m vocalinux.main + +# Run from source with debug logging +run-source-debug: + python -m vocalinux.main --debug + +# Run pre-commit hooks on all files +pre-commit: + pre-commit run --all-files + +# Print the current version +version: + @python -c "from src.vocalinux.version import __version__; print(__version__)"