chore(build): harden installer script and replace Makefile with Justfile
Stacked bottom of #701. Replaces Makefile with justfile and hardens install.sh (strict mode, logging, HTTP probes, curl -f on model downloads, no v0.10.1-beta fallback). Closes #700.
This commit is contained in:
+1
-1
@@ -29,7 +29,7 @@ ci:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- ".github/**/*"
|
||||
- "Makefile"
|
||||
- "justfile"
|
||||
- "scripts/**/*"
|
||||
- ".flake8"
|
||||
- ".pre-commit-config.yaml"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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__)"
|
||||
@@ -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 ..
|
||||
|
||||
+164
-63
@@ -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-<timestamp>.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
|
||||
|
||||
@@ -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__)"
|
||||
Reference in New Issue
Block a user