* fix(install): leftover review notes from #700 and #705 The scratch dir now holds pip logs and model download temps, so a failed run has something to inspect. Dep, venv, leftover-process, and pip failures use exit codes 2/3/4 instead of a blanket 1. --help says the transcript is written during installation, and the tag hint is --tag=<release>. AGENTS.md no longer claims the hash-pinned exports are consumed (they wait for phases 2/3/5). Docs add a just install line and vocalinux[vosk] next to the PyPI commands. Settings points at --engine=whisper. The whisper.cpp fallback installs ".[vosk]". Fixes leftover review notes from #700 and #705. Refs #701. * fix(install): do not let pip build PyGObject from sdist Fresh --auto --skip-system-deps died on pygobject-3.56.3 even with distro python3-gi present. Install the other runtime deps from pyproject.toml, then pip install --no-deps. Extra installs use the extra's packages only, not pip install ".[extra]". Fixes leftover review notes from #700 and #705. Refs #701. * fix(install): tolerate unset XDG_SESSION_TYPE under set -u --auto with DISPLAY but no login session died on XDG_SESSION_TYPE: unbound variable. The session probe now uses ${XDG_SESSION_TYPE:-}, ${WAYLAND_DISPLAY:-}, and ${DISPLAY:-}.
This commit is contained in:
@@ -27,6 +27,7 @@ Vocalinux is a voice dictation system for Linux. It uses:
|
||||
# Or manually:
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# just: https://just.systems or the distro package `just`
|
||||
# Dependency lock files (see "Dependency Management" below)
|
||||
just lock # regenerate uv.lock + requirements/*.txt after changing deps
|
||||
just lock-check # fail if uv.lock is stale relative to pyproject.toml
|
||||
@@ -91,12 +92,14 @@ npm run test # Jest tests
|
||||
|
||||
## Dependency Management (uv + lockfiles)
|
||||
|
||||
All dependency versions are pinned. The source of truth is `uv.lock`; the
|
||||
`requirements/*.txt` files are generated hash-pinned exports consumed by `install.sh`,
|
||||
the AppImage build, and CI. **Never edit `requirements/*.txt` by hand** — change
|
||||
`pyproject.toml` (or `requirements/whisper.in` for the whisper engine), run `just lock`,
|
||||
and commit the result together with the manifest change. uv itself is version-pinned
|
||||
via `[tool.uv]` in `pyproject.toml`.
|
||||
The source of truth is `uv.lock`. `just lock` regenerates it and the
|
||||
`requirements/*.txt` hash-pinned exports. Those exports exist for later
|
||||
packaging work (`install.sh`, AppImage, CI; phases 2, 3, and 5 of #701)
|
||||
and are unused until those phases land. Do not edit `requirements/*.txt`
|
||||
by hand. Change `pyproject.toml` (or `requirements/whisper.in` for the
|
||||
whisper engine), run `just lock`, and commit the lock plus the exports
|
||||
with the manifest change. uv itself is version-pinned via `[tool.uv]`
|
||||
in `pyproject.toml`.
|
||||
|
||||
- **PyGObject always comes from the distro** (`python3-gi` through a
|
||||
`--system-site-packages` venv). It cannot be pip-installed on Ubuntu 24.04, and
|
||||
|
||||
@@ -224,8 +224,8 @@ curl -fsSL https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh |
|
||||
|
||||
### Important: Install System Packages First
|
||||
|
||||
Before running `pip install vocalinux` or `pipx install vocalinux`, you **must**
|
||||
install the required system packages:
|
||||
Before running `pip install vocalinux`, `pip install "vocalinux[vosk]"`, or
|
||||
`pipx install vocalinux`, you **must** install the required system packages:
|
||||
|
||||
#### Ubuntu/Debian
|
||||
```bash
|
||||
@@ -278,6 +278,7 @@ sudo zypper install "${PYVER}-gobject" "${PYVER}-gobject-cairo" gtk3 \
|
||||
3. **Install Vocalinux from PyPI**:
|
||||
```bash
|
||||
pip install vocalinux
|
||||
# vosk engine: pip install "vocalinux[vosk]"
|
||||
```
|
||||
|
||||
4. **Run Vocalinux**:
|
||||
@@ -307,6 +308,7 @@ sudo zypper install "${PYVER}-gobject" "${PYVER}-gobject-cairo" gtk3 \
|
||||
3. **Install Vocalinux**:
|
||||
```bash
|
||||
pipx install vocalinux
|
||||
# vosk engine: pipx install "vocalinux[vosk]"
|
||||
```
|
||||
|
||||
4. **Run Vocalinux**:
|
||||
|
||||
+6
-1
@@ -64,6 +64,7 @@ python3 -m venv ~/.local/share/vocalinux-pypi/venv --system-site-packages
|
||||
source ~/.local/share/vocalinux-pypi/venv/bin/activate
|
||||
pip install --upgrade pip setuptools wheel
|
||||
pip install vocalinux
|
||||
# vosk engine: pip install "vocalinux[vosk]"
|
||||
vocalinux
|
||||
```
|
||||
|
||||
@@ -79,6 +80,7 @@ python3 -m venv ~/.local/share/vocalinux-pypi/venv --system-site-packages
|
||||
source ~/.local/share/vocalinux-pypi/venv/bin/activate
|
||||
pip install --upgrade pip setuptools wheel
|
||||
pip install vocalinux
|
||||
# vosk engine: pip install "vocalinux[vosk]"
|
||||
vocalinux
|
||||
```
|
||||
|
||||
@@ -99,6 +101,7 @@ python -m venv ~/.local/share/vocalinux-pypi/venv --system-site-packages
|
||||
source ~/.local/share/vocalinux-pypi/venv/bin/activate
|
||||
pip install --upgrade pip setuptools wheel
|
||||
pip install vocalinux
|
||||
# vosk engine: pip install "vocalinux[vosk]"
|
||||
vocalinux
|
||||
```
|
||||
|
||||
@@ -118,6 +121,7 @@ python3 -m venv ~/.local/share/vocalinux-pypi/venv --system-site-packages
|
||||
source ~/.local/share/vocalinux-pypi/venv/bin/activate
|
||||
pip install --upgrade pip setuptools wheel
|
||||
pip install vocalinux
|
||||
# vosk engine: pip install "vocalinux[vosk]"
|
||||
vocalinux
|
||||
```
|
||||
|
||||
@@ -431,7 +435,8 @@ pip install ".[vad]"
|
||||
pip install -e ".[dev,vad]"
|
||||
```
|
||||
|
||||
For PyPI instead of a local checkout, use `pip install vocalinux` after installing
|
||||
For PyPI instead of a local checkout, use `pip install vocalinux` (or
|
||||
`pip install "vocalinux[vosk]"` for the vosk engine) after installing
|
||||
the same system packages and creating the virtual environment with
|
||||
`--system-site-packages`.
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ Use this checklist for every release:
|
||||
- [ ] Confirm install docs/website mention **AppImage** when this release ships AppImages
|
||||
|
||||
### Testing & Verification
|
||||
Install `just` from https://just.systems or the distro package `just`.
|
||||
- [ ] Run `just test` - All tests pass
|
||||
- [ ] Run `just lint` - No linting errors
|
||||
- [ ] Run `just typecheck` - No type errors
|
||||
@@ -258,7 +259,8 @@ Also align `web/package-lock.json` top-level `version` fields with `web/package.
|
||||
|
||||
### Step 5: Verify Everything Works
|
||||
|
||||
Run the full verification suite:
|
||||
Run the full verification suite. Install `just` from
|
||||
https://just.systems or the distro package `just`.
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
|
||||
+140
-61
@@ -216,7 +216,7 @@ check_running_processes() {
|
||||
if [ -n "$FINAL_PIDS" ]; then
|
||||
print_error "Could not terminate all Vocalinux processes: $FINAL_PIDS"
|
||||
print_error "Please manually kill these processes and run the installer again."
|
||||
exit 1
|
||||
exit "$EXIT_USER_ABORT"
|
||||
else
|
||||
print_success "All Vocalinux processes stopped"
|
||||
fi
|
||||
@@ -349,7 +349,7 @@ while [[ $# -gt 0 ]]; do
|
||||
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 "During installation a full transcript is saved to"
|
||||
echo " ~/.local/state/vocalinux/install-<timestamp>.log"
|
||||
echo ""
|
||||
echo "Exit codes:"
|
||||
@@ -384,9 +384,11 @@ 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.
|
||||
# Scratch directory for pip logs, model downloads, and other temps.
|
||||
# Removed on success; kept for inspection (with a notice) when the install fails.
|
||||
# Exporting TMPDIR routes pip/wget/curl scratch files into this directory.
|
||||
VOCALINUX_TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/vocalinux-install.XXXXXXXX")"
|
||||
export TMPDIR="$VOCALINUX_TMP_DIR"
|
||||
|
||||
cleanup_on_exit() {
|
||||
local rc=$?
|
||||
@@ -594,7 +596,7 @@ 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"
|
||||
print_error "Re-run with an explicit release tag, e.g.: --tag=<release>"
|
||||
exit "$EXIT_NETWORK"
|
||||
fi
|
||||
print_info "Installing Vocalinux version: ${INSTALL_TAG}"
|
||||
@@ -1766,7 +1768,7 @@ install_system_dependencies() {
|
||||
|
||||
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 update || { print_error "Failed to update package lists"; exit "$EXIT_NETWORK"; }
|
||||
|
||||
# Handle appindicator package for Ubuntu (old package deprecated in newer releases)
|
||||
if echo "$MISSING_PACKAGES" | grep -q "gir1.2-appindicator3-0.1"; then
|
||||
@@ -1776,16 +1778,16 @@ install_system_dependencies() {
|
||||
print_info "gir1.2-appindicator3-0.1 not available, trying gir1.2-ayatanaappindicator3-0.1..."
|
||||
if ! DEBIAN_FRONTEND=noninteractive sudo apt install -y gir1.2-ayatanaappindicator3-0.1; then
|
||||
print_error "Failed to install appindicator package (tried both gir1.2-appindicator3-0.1 and gir1.2-ayatanaappindicator3-0.1)"
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
fi
|
||||
print_info "Successfully installed gir1.2-ayatanaappindicator3-0.1 (modern replacement)"
|
||||
fi
|
||||
|
||||
if [ -n "$FILTERED_PACKAGES" ]; then
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt install -y $FILTERED_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt install -y $FILTERED_PACKAGES || { print_error "Failed to install dependencies"; exit "$EXIT_MISSING_DEPS"; }
|
||||
fi
|
||||
else
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt install -y $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt install -y $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit "$EXIT_MISSING_DEPS"; }
|
||||
fi
|
||||
else
|
||||
print_info "All required packages are already installed."
|
||||
@@ -1802,7 +1804,7 @@ install_system_dependencies() {
|
||||
UPDATE_CMD="sudo yum check-update"
|
||||
else
|
||||
print_error "No supported package manager found (dnf/yum)"
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
fi
|
||||
|
||||
# Check for missing packages
|
||||
@@ -1815,19 +1817,19 @@ install_system_dependencies() {
|
||||
if [ -n "$MISSING_PACKAGES" ]; then
|
||||
print_info "Installing missing packages:$MISSING_PACKAGES"
|
||||
$UPDATE_CMD || true # dnf check-update returns 100 if updates available
|
||||
$INSTALL_CMD $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||||
$INSTALL_CMD $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit "$EXIT_MISSING_DEPS"; }
|
||||
else
|
||||
print_info "All required packages are already installed."
|
||||
fi
|
||||
|
||||
install_preferred_appindicator "$INSTALL_CMD" "libayatana-appindicator-gtk3" "libappindicator-gtk3" dnf_package_installed || exit 1
|
||||
install_preferred_appindicator "$INSTALL_CMD" "libayatana-appindicator-gtk3" "libappindicator-gtk3" dnf_package_installed || exit "$EXIT_MISSING_DEPS"
|
||||
;;
|
||||
|
||||
arch)
|
||||
# For Arch-based systems
|
||||
if ! command_exists pacman; then
|
||||
print_error "Pacman package manager not found"
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
fi
|
||||
|
||||
# Check for missing packages
|
||||
@@ -1840,19 +1842,19 @@ install_system_dependencies() {
|
||||
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; }
|
||||
sudo pacman -S --noconfirm $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit "$EXIT_MISSING_DEPS"; }
|
||||
else
|
||||
print_info "All required packages are already installed."
|
||||
fi
|
||||
|
||||
install_preferred_appindicator "sudo pacman -S --noconfirm" "libayatana-appindicator" "libappindicator-gtk3" pacman_package_installed || exit 1
|
||||
install_preferred_appindicator "sudo pacman -S --noconfirm" "libayatana-appindicator" "libappindicator-gtk3" pacman_package_installed || exit "$EXIT_MISSING_DEPS"
|
||||
;;
|
||||
|
||||
suse)
|
||||
# For openSUSE
|
||||
if ! command_exists zypper; then
|
||||
print_error "Zypper package manager not found"
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
fi
|
||||
|
||||
sudo zypper refresh || true
|
||||
@@ -1872,7 +1874,7 @@ install_system_dependencies() {
|
||||
print_info "Installing missing packages: ${MISSING_ZYPPER_PACKAGES[*]}"
|
||||
sudo zypper install -y "${MISSING_ZYPPER_PACKAGES[@]}" || {
|
||||
print_error "Failed to install openSUSE base dependencies"
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
}
|
||||
else
|
||||
print_info "All base openSUSE packages are already installed."
|
||||
@@ -1896,22 +1898,22 @@ install_system_dependencies() {
|
||||
|
||||
if ! suse_install_first_available "Python pip" "${PY_PIP_CANDIDATES[@]}"; then
|
||||
print_error "Failed to install Python pip package (tried: ${PY_PIP_CANDIDATES[*]})"
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
fi
|
||||
|
||||
if ! suse_install_first_available "PyGObject bindings" "${PY_GOBJECT_CANDIDATES[@]}"; then
|
||||
print_error "Failed to install PyGObject package (tried: ${PY_GOBJECT_CANDIDATES[*]})"
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
fi
|
||||
|
||||
if ! suse_install_first_available "PyGObject Cairo bindings" "${PY_GOBJECT_CAIRO_CANDIDATES[@]}"; then
|
||||
print_error "Failed to install PyGObject Cairo package (tried: ${PY_GOBJECT_CAIRO_CANDIDATES[*]})"
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
fi
|
||||
|
||||
if ! suse_install_first_available "Python development headers" "${PY_DEVEL_CANDIDATES[@]}"; then
|
||||
print_error "Failed to install Python development headers (tried: ${PY_DEVEL_CANDIDATES[*]})"
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
fi
|
||||
|
||||
if ! suse_install_first_available "Python virtualenv/venv" "${PY_VIRTUALENV_CANDIDATES[@]}" "${PY_VENV_CANDIDATES[@]}"; then
|
||||
@@ -1922,7 +1924,7 @@ install_system_dependencies() {
|
||||
if ! suse_install_appindicator_runtime; then
|
||||
print_error "Failed to install a working AppIndicator/Ayatana GI runtime on openSUSE."
|
||||
print_error "Try manually: sudo zypper install typelib-1_0-AyatanaAppIndicator3-0_1 libayatana-appindicator3-1"
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
fi
|
||||
|
||||
if [[ "${SELECTED_ENGINE:-whisper_cpp}" == "whisper_cpp" && "${WHISPERCPP_BACKEND:-}" != "cpu" ]]; then
|
||||
@@ -1943,7 +1945,7 @@ install_system_dependencies() {
|
||||
# For Gentoo Linux
|
||||
if ! command_exists emerge; then
|
||||
print_error "Emerge package manager not found"
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
fi
|
||||
|
||||
print_info "Gentoo detected. Installing dependencies..."
|
||||
@@ -1961,9 +1963,9 @@ install_system_dependencies() {
|
||||
if [ -n "$MISSING_PACKAGES" ]; then
|
||||
print_info "Installing packages:$MISSING_PACKAGES"
|
||||
# Update Portage tree first
|
||||
sudo emerge --sync || { print_error "Failed to sync Portage tree"; exit 1; }
|
||||
sudo emerge --sync || { print_error "Failed to sync Portage tree"; exit "$EXIT_NETWORK"; }
|
||||
# Install missing packages
|
||||
sudo emerge $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||||
sudo emerge $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit "$EXIT_MISSING_DEPS"; }
|
||||
else
|
||||
print_info "All required packages are already installed."
|
||||
fi
|
||||
@@ -1973,7 +1975,7 @@ install_system_dependencies() {
|
||||
# For Alpine Linux
|
||||
if ! command_exists apk; then
|
||||
print_error "Apk package manager not found"
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
fi
|
||||
|
||||
print_info "Alpine Linux detected."
|
||||
@@ -1989,8 +1991,8 @@ install_system_dependencies() {
|
||||
|
||||
if [ -n "$MISSING_PACKAGES" ]; then
|
||||
print_info "Installing packages:$MISSING_PACKAGES"
|
||||
sudo apk update || { print_error "Failed to update package indexes"; exit 1; }
|
||||
sudo apk add $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||||
sudo apk update || { print_error "Failed to update package indexes"; exit "$EXIT_NETWORK"; }
|
||||
sudo apk add $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit "$EXIT_MISSING_DEPS"; }
|
||||
else
|
||||
print_info "All required packages are already installed."
|
||||
fi
|
||||
@@ -2000,7 +2002,7 @@ install_system_dependencies() {
|
||||
# For Void Linux
|
||||
if ! command_exists xbps; then
|
||||
print_error "Xbps package manager not found"
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
fi
|
||||
|
||||
print_info "Void Linux detected."
|
||||
@@ -2015,7 +2017,7 @@ install_system_dependencies() {
|
||||
|
||||
if [ -n "$MISSING_PACKAGES" ]; then
|
||||
print_info "Installing packages:$MISSING_PACKAGES"
|
||||
sudo xbps-install -Sy $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||||
sudo xbps-install -Sy $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit "$EXIT_MISSING_DEPS"; }
|
||||
else
|
||||
print_info "All required packages are already installed."
|
||||
fi
|
||||
@@ -2025,7 +2027,7 @@ install_system_dependencies() {
|
||||
# For Solus
|
||||
if ! command_exists eopkg; then
|
||||
print_error "Eopkg package manager not found"
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
fi
|
||||
|
||||
print_info "Solus detected."
|
||||
@@ -2040,7 +2042,7 @@ install_system_dependencies() {
|
||||
|
||||
if [ -n "$MISSING_PACKAGES" ]; then
|
||||
print_info "Installing packages:$MISSING_PACKAGES"
|
||||
sudo eopkg install $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||||
sudo eopkg install $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit "$EXIT_MISSING_DEPS"; }
|
||||
else
|
||||
print_info "All required packages are already installed."
|
||||
fi
|
||||
@@ -2056,7 +2058,7 @@ install_system_dependencies() {
|
||||
UPDATE_CMD="sudo urpmi.update -a"
|
||||
else
|
||||
print_error "No supported package manager found (dnf/urpmi)"
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
fi
|
||||
|
||||
# Use similar packages to Fedora/RHEL
|
||||
@@ -2070,7 +2072,7 @@ install_system_dependencies() {
|
||||
if [ -n "$MISSING_PACKAGES" ]; then
|
||||
print_info "Installing missing packages:$MISSING_PACKAGES"
|
||||
$UPDATE_CMD 2>/dev/null || true
|
||||
$INSTALL_CMD $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||||
$INSTALL_CMD $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit "$EXIT_MISSING_DEPS"; }
|
||||
else
|
||||
print_info "All required packages are already installed."
|
||||
fi
|
||||
@@ -2135,14 +2137,15 @@ 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 XDG_SESSION_TYPE first. These are often unset without a login
|
||||
# session even when DISPLAY is set; ${var:-} keeps set -u from aborting.
|
||||
if [ -n "${XDG_SESSION_TYPE:-}" ]; then
|
||||
SESSION_TYPE="${XDG_SESSION_TYPE:-}"
|
||||
# Check for Wayland-specific environment variables
|
||||
elif [ -n "$WAYLAND_DISPLAY" ]; then
|
||||
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
|
||||
elif [ -n "${DISPLAY:-}" ] && command_exists xset && xset q &>/dev/null; then
|
||||
SESSION_TYPE="x11"
|
||||
# Check loginctl if available
|
||||
elif command_exists loginctl; then
|
||||
@@ -2442,7 +2445,7 @@ setup_virtual_environment() {
|
||||
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
|
||||
# In non-interactive mode, reuse existing venv
|
||||
print_info "Non-interactive mode: using existing virtual environment."
|
||||
source "$VENV_DIR/bin/activate" || { print_error "Failed to activate virtual environment"; exit 1; }
|
||||
source "$VENV_DIR/bin/activate" || { print_error "Failed to activate virtual environment"; exit "$EXIT_MISSING_DEPS"; }
|
||||
return 0
|
||||
else
|
||||
read -p "Do you want to recreate it? (y/n) " -n 1 -r
|
||||
@@ -2452,7 +2455,7 @@ setup_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; }
|
||||
source "$VENV_DIR/bin/activate" || { print_error "Failed to activate virtual environment"; exit "$EXIT_MISSING_DEPS"; }
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
@@ -2465,16 +2468,16 @@ setup_virtual_environment() {
|
||||
print_warning "python3 -m venv failed, trying python3 -m virtualenv..."
|
||||
python3 -m virtualenv --system-site-packages "$VENV_DIR" || {
|
||||
print_error "Failed to create virtual environment. Please check your Python installation."
|
||||
exit 1
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
}
|
||||
}
|
||||
|
||||
# Activate virtual environment
|
||||
source "$VENV_DIR/bin/activate" || { print_error "Failed to activate virtual environment"; exit 1; }
|
||||
source "$VENV_DIR/bin/activate" || { print_error "Failed to activate virtual environment"; exit "$EXIT_MISSING_DEPS"; }
|
||||
|
||||
# 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; }
|
||||
pip install --upgrade pip setuptools wheel || { print_error "Failed to update pip, setuptools, and wheel"; exit "$EXIT_NETWORK"; }
|
||||
|
||||
print_info "Virtual environment activated successfully."
|
||||
}
|
||||
@@ -2931,7 +2934,7 @@ install_whispercpp_with_gpu_support() {
|
||||
# vosk is an optional extra; make sure it is importable before
|
||||
# falling back to it
|
||||
if ! "$VENV_DIR/bin/python" -c "import vosk" 2>/dev/null; then
|
||||
pip install vosk --log "$PIP_LOG_FILE" || \
|
||||
pip_install_extras_skip_pygobject "$PIP_LOG_FILE" vosk || \
|
||||
print_warning "Could not install vosk; install it manually or pick another engine in Settings."
|
||||
fi
|
||||
|
||||
@@ -2973,10 +2976,88 @@ FALLBACK_VOSK_CONFIG
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Distro python3-gi provides `gi`, but apt does not drop pip-visible
|
||||
# PyGObject dist-info. `pip install .` then tries to build pygobject from
|
||||
# sdist and dies (needs girepository-2.0). Same skip as uv export's
|
||||
# --no-emit-package pygobject: install the other deps, then the project
|
||||
# with --no-deps. Do not use requirements/*.txt hashes here (Phase 2).
|
||||
write_pip_reqs_skip_pygobject() {
|
||||
local dest="$1"
|
||||
shift
|
||||
"$VENV_DIR/bin/python" - "$dest" "$@" <<'PY'
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
dest = Path(sys.argv[1])
|
||||
extras = sys.argv[2:]
|
||||
text = Path("pyproject.toml").read_text()
|
||||
|
||||
|
||||
def quoted_strings(block: str):
|
||||
return re.findall(r'"([^"]+)"', block)
|
||||
|
||||
|
||||
reqs = []
|
||||
if not extras:
|
||||
match = re.search(r"^dependencies = \[(.*?)\]", text, re.M | re.S)
|
||||
for req in quoted_strings(match.group(1) if match else ""):
|
||||
pkg = re.split(r"[<>=!~;\[]", req, 1)[0].strip()
|
||||
if pkg.lower() == "pygobject":
|
||||
continue
|
||||
reqs.append(req)
|
||||
else:
|
||||
opt = re.search(
|
||||
r"^\[project\.optional-dependencies\](.*?)(\n\[|\Z)", text, re.M | re.S
|
||||
)
|
||||
opt_text = opt.group(1) if opt else ""
|
||||
for extra in extras:
|
||||
match = re.search(rf"^{re.escape(extra)} = \[(.*?)\]", opt_text, re.M | re.S)
|
||||
if match:
|
||||
reqs.extend(quoted_strings(match.group(1)))
|
||||
|
||||
dest.write_text("\n".join(reqs) + ("\n" if reqs else ""))
|
||||
PY
|
||||
}
|
||||
|
||||
require_distro_gi() {
|
||||
if ! "$VENV_DIR/bin/python" -c "import gi" 2>/dev/null; then
|
||||
print_error "Distro PyGObject (python3-gi / python3-gobject) is not importable in the venv."
|
||||
print_error "Install it with your package manager. Pip cannot build PyGObject here."
|
||||
exit "$EXIT_MISSING_DEPS"
|
||||
fi
|
||||
}
|
||||
|
||||
pip_install_reqs_file() {
|
||||
local pip_log="$1"
|
||||
local reqs_file="$2"
|
||||
if [ ! -s "$reqs_file" ]; then
|
||||
return 0
|
||||
fi
|
||||
pip install -r "$reqs_file" --log "$pip_log"
|
||||
}
|
||||
|
||||
pip_install_project_skip_pygobject() {
|
||||
local pip_log="$1"
|
||||
shift
|
||||
require_distro_gi
|
||||
write_pip_reqs_skip_pygobject "$VOCALINUX_TMP_DIR/runtime-deps.txt"
|
||||
pip_install_reqs_file "$pip_log" "$VOCALINUX_TMP_DIR/runtime-deps.txt" || return 1
|
||||
pip install --no-deps --log "$pip_log" "$@"
|
||||
}
|
||||
|
||||
pip_install_extras_skip_pygobject() {
|
||||
local pip_log="$1"
|
||||
shift
|
||||
write_pip_reqs_skip_pygobject "$VOCALINUX_TMP_DIR/extra-deps.txt" "$@"
|
||||
pip_install_reqs_file "$pip_log" "$VOCALINUX_TMP_DIR/extra-deps.txt"
|
||||
}
|
||||
|
||||
# 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)
|
||||
# Pip logs live in the install scratch dir so a failed run can keep them.
|
||||
local PIP_LOG_DIR="$VOCALINUX_TMP_DIR/pip"
|
||||
mkdir -p "$PIP_LOG_DIR"
|
||||
local PIP_LOG_FILE="$PIP_LOG_DIR/pip_log.txt"
|
||||
|
||||
# Detect GI_TYPELIB_PATH early for cross-distro compatibility
|
||||
@@ -3009,10 +3090,8 @@ install_python_package() {
|
||||
|
||||
print_info "Installing neural VAD support (Silero / ONNX Runtime)..."
|
||||
local VAD_INSTALL_SUCCESS=false
|
||||
if [[ "$EDITABLE_MODE" == "yes" ]]; then
|
||||
pip install -e ".[vad]" --log "$PIP_LOG_FILE" && VAD_INSTALL_SUCCESS=true
|
||||
else
|
||||
pip install ".[vad]" --log "$PIP_LOG_FILE" && VAD_INSTALL_SUCCESS=true
|
||||
if pip_install_extras_skip_pygobject "$PIP_LOG_FILE" vad; then
|
||||
VAD_INSTALL_SUCCESS=true
|
||||
fi
|
||||
|
||||
if [[ "$VAD_INSTALL_SUCCESS" == "true" ]]; then
|
||||
@@ -3037,7 +3116,7 @@ PY
|
||||
print_info "Installing Vocalinux in development mode..."
|
||||
|
||||
# Install in development mode with logging
|
||||
pip install -e . --log "$PIP_LOG_FILE" || {
|
||||
pip_install_project_skip_pygobject "$PIP_LOG_FILE" -e . || {
|
||||
print_error "Failed to install Vocalinux in development mode."
|
||||
print_error "Check the pip log for details: $PIP_LOG_FILE"
|
||||
return 1
|
||||
@@ -3051,7 +3130,7 @@ PY
|
||||
|
||||
# Install all optional dependencies for development
|
||||
print_info "Installing all optional dependencies for development..."
|
||||
pip install -e ".[whisper,dev]" --log "$PIP_LOG_FILE" || {
|
||||
pip_install_extras_skip_pygobject "$PIP_LOG_FILE" whisper dev || {
|
||||
print_warning "Failed to install some optional dependencies."
|
||||
print_warning "Some features may not work correctly."
|
||||
}
|
||||
@@ -3065,7 +3144,7 @@ PY
|
||||
print_info "Installing Vocalinux..."
|
||||
|
||||
# Install the package with logging (includes pywhispercpp by default)
|
||||
pip install . --log "$PIP_LOG_FILE" || {
|
||||
pip_install_project_skip_pygobject "$PIP_LOG_FILE" . || {
|
||||
print_error "Failed to install Vocalinux."
|
||||
print_error "Check the pip log for details: $PIP_LOG_FILE"
|
||||
return 1
|
||||
@@ -3208,7 +3287,7 @@ FALLBACK_CONFIG
|
||||
print_info "VOSK is fast and works well on older systems."
|
||||
|
||||
# vosk is an optional extra; install it alongside the base package
|
||||
pip install ".[vosk]" --log "$PIP_LOG_FILE" || {
|
||||
pip_install_extras_skip_pygobject "$PIP_LOG_FILE" vosk || {
|
||||
print_error "Failed to install the vosk engine"
|
||||
return 1
|
||||
}
|
||||
@@ -3325,8 +3404,8 @@ REMOTE_CONFIG
|
||||
# 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"
|
||||
# Pip logs stay under VOCALINUX_TMP_DIR; the EXIT trap removes that
|
||||
# directory on success and keeps it when a later step fails.
|
||||
|
||||
# GI_TYPELIB_PATH was already detected at the start of install_python_package
|
||||
|
||||
@@ -3441,7 +3520,7 @@ WRAPPER_EOF
|
||||
# Install Python package
|
||||
if ! install_python_package; then
|
||||
print_error "Failed to install Vocalinux package. Installation cannot continue."
|
||||
exit 1
|
||||
exit "$EXIT_NETWORK"
|
||||
fi
|
||||
|
||||
# Function to download and install Whisper tiny model
|
||||
@@ -3479,7 +3558,7 @@ install_whisper_model() {
|
||||
print_info "Downloading Whisper tiny model..."
|
||||
print_info "This may take a few minutes depending on your internet connection."
|
||||
|
||||
local TEMP_FILE="$TINY_MODEL_PATH.tmp"
|
||||
local TEMP_FILE="$VOCALINUX_TMP_DIR/tiny.pt"
|
||||
|
||||
# Download the model
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
@@ -3558,7 +3637,7 @@ install_vosk_models() {
|
||||
print_info "Downloading small VOSK model (approximately 40MB)..."
|
||||
print_info "This may take a few minutes depending on your internet connection."
|
||||
|
||||
local TEMP_ZIP="$MODELS_DIR/$(basename $SMALL_MODEL_URL)"
|
||||
local TEMP_ZIP="$VOCALINUX_TMP_DIR/$(basename $SMALL_MODEL_URL)"
|
||||
|
||||
# Download the model
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
@@ -3653,7 +3732,7 @@ install_whispercpp_model() {
|
||||
print_info "Downloading whisper.cpp tiny model..."
|
||||
print_info "This may take a few minutes depending on your internet connection."
|
||||
|
||||
local TEMP_FILE="$TINY_MODEL_PATH.tmp"
|
||||
local TEMP_FILE="$VOCALINUX_TMP_DIR/ggml-tiny.bin"
|
||||
|
||||
# Download the model
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
|
||||
@@ -5005,7 +5005,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
Installation Options:
|
||||
|
||||
1. Using the installation script:
|
||||
./install.sh --with-whisper
|
||||
./install.sh --engine=whisper
|
||||
|
||||
2. Manual installation in virtual environment:
|
||||
source venv/bin/activate
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
"""Source checks for leftover installer review notes from #700 and #705."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
INSTALLER = Path(__file__).resolve().parents[1] / "install.sh"
|
||||
SETTINGS = Path(__file__).resolve().parents[1] / "src" / "vocalinux" / "ui" / "settings_dialog.py"
|
||||
AGENTS = Path(__file__).resolve().parents[1] / "AGENTS.md"
|
||||
|
||||
|
||||
def _installer_source() -> str:
|
||||
return INSTALLER.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_help_places_transcript_under_installation() -> None:
|
||||
result = subprocess.run(
|
||||
["bash", str(INSTALLER), "--help"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "During installation a full transcript is saved to" in result.stdout
|
||||
assert "every run" not in result.stdout
|
||||
|
||||
|
||||
def test_help_does_not_create_install_log_or_scratch_dir(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path / "state"))
|
||||
monkeypatch.setenv("TMPDIR", str(tmp_path / "tmp"))
|
||||
(tmp_path / "tmp").mkdir()
|
||||
|
||||
result = subprocess.run(
|
||||
["bash", str(INSTALLER), "--help"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert not list((tmp_path / "state").rglob("install-*.log"))
|
||||
assert not list((tmp_path / "tmp").glob("vocalinux-install.*"))
|
||||
|
||||
|
||||
def test_scratch_dir_owns_pip_logs_and_model_temps() -> None:
|
||||
source = _installer_source()
|
||||
assert 'PIP_LOG_DIR="$VOCALINUX_TMP_DIR/pip"' in source
|
||||
assert 'export TMPDIR="$VOCALINUX_TMP_DIR"' in source
|
||||
assert 'TEMP_FILE="$VOCALINUX_TMP_DIR/tiny.pt"' in source
|
||||
assert 'TEMP_FILE="$VOCALINUX_TMP_DIR/ggml-tiny.bin"' in source
|
||||
assert 'TEMP_ZIP="$VOCALINUX_TMP_DIR/$(basename $SMALL_MODEL_URL)"' in source
|
||||
|
||||
|
||||
def test_system_dep_and_venv_failures_use_structured_exit_codes() -> None:
|
||||
source = _installer_source()
|
||||
assert 'print_error "Failed to install dependencies"; exit "$EXIT_MISSING_DEPS"' in source
|
||||
assert 'print_error "Failed to update package lists"; exit "$EXIT_NETWORK"' in source
|
||||
assert (
|
||||
'print_error "Failed to create virtual environment. Please check your Python installation."'
|
||||
in source
|
||||
)
|
||||
assert 'exit "$EXIT_MISSING_DEPS"' in source
|
||||
assert (
|
||||
'print_error "Failed to update pip, setuptools, and wheel"; exit "$EXIT_NETWORK"' in source
|
||||
)
|
||||
assert (
|
||||
'print_error "Failed to install Vocalinux package. Installation cannot continue."' in source
|
||||
)
|
||||
assert 'exit "$EXIT_NETWORK"' in source
|
||||
assert "Could not terminate all Vocalinux processes" in source
|
||||
assert 'exit "$EXIT_USER_ABORT"' in source
|
||||
|
||||
|
||||
def test_release_tag_hint_is_not_hardcoded() -> None:
|
||||
source = _installer_source()
|
||||
assert "--tag=<release>" in source
|
||||
assert "--tag=v0.15.0" not in source
|
||||
|
||||
|
||||
def test_whispercpp_fallback_installs_vosk_extra() -> None:
|
||||
source = _installer_source()
|
||||
assert 'pip_install_extras_skip_pygobject "$PIP_LOG_FILE" vosk' in source
|
||||
assert "pip install vosk --log" not in source
|
||||
assert 'pip install ".[vosk]"' not in source
|
||||
|
||||
|
||||
def test_project_pip_install_skips_pygobject() -> None:
|
||||
source = _installer_source()
|
||||
assert "write_pip_reqs_skip_pygobject()" in source
|
||||
assert "pip_install_project_skip_pygobject()" in source
|
||||
assert "pip install --no-deps" in source
|
||||
assert "--no-emit-package pygobject" in source
|
||||
assert 'pip install . --log "$PIP_LOG_FILE"' not in source
|
||||
assert 'pip install -e . --log "$PIP_LOG_FILE"' not in source
|
||||
assert 'pip install ".[vad]"' not in source
|
||||
assert 'pip install -e ".[vad]"' not in source
|
||||
assert 'pip install -e ".[whisper,dev]"' not in source
|
||||
|
||||
|
||||
def _run_reqs_writer(dest: Path, *extras: str) -> None:
|
||||
source = _installer_source()
|
||||
start = source.index("from pathlib import Path\nimport re\nimport sys\n")
|
||||
end = source.index("\nPY\n}", start)
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-", str(dest), *extras],
|
||||
check=False,
|
||||
cwd=INSTALLER.parent,
|
||||
input=source[start:end],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
def test_write_pip_reqs_skip_pygobject_runtime(tmp_path) -> None:
|
||||
dest = tmp_path / "runtime.txt"
|
||||
_run_reqs_writer(dest)
|
||||
reqs = dest.read_text().splitlines()
|
||||
assert reqs
|
||||
assert all("pygobject" not in req.lower() for req in reqs)
|
||||
assert any(req.startswith("pyaudio") for req in reqs)
|
||||
assert any(req.startswith("pywhispercpp") for req in reqs)
|
||||
|
||||
|
||||
def test_write_pip_reqs_skip_pygobject_vosk_extra(tmp_path) -> None:
|
||||
dest = tmp_path / "vosk.txt"
|
||||
_run_reqs_writer(dest, "vosk")
|
||||
reqs = dest.read_text().splitlines()
|
||||
assert reqs == ["vosk>=0.3.45"]
|
||||
|
||||
|
||||
def test_settings_uses_engine_flag_not_removed_with_whisper() -> None:
|
||||
text = SETTINGS.read_text(encoding="utf-8")
|
||||
assert "./install.sh --engine=whisper" in text
|
||||
assert "--with-whisper" not in text
|
||||
|
||||
|
||||
def test_session_type_env_reads_use_default_expansion() -> None:
|
||||
source = _installer_source()
|
||||
start = source.index("install_text_input_tools()")
|
||||
end = source.index('print_info "Detected session type:', start)
|
||||
block = source[start:end]
|
||||
assert '[ -n "${XDG_SESSION_TYPE:-}" ]' in block
|
||||
assert '[ -n "${WAYLAND_DISPLAY:-}" ]' in block
|
||||
assert '[ -n "${DISPLAY:-}" ]' in block
|
||||
assert '"$XDG_SESSION_TYPE"' not in block
|
||||
assert '"$WAYLAND_DISPLAY"' not in block
|
||||
assert '"$DISPLAY"' not in block
|
||||
|
||||
env = os.environ.copy()
|
||||
env.pop("XDG_SESSION_TYPE", None)
|
||||
env.pop("WAYLAND_DISPLAY", None)
|
||||
env["DISPLAY"] = ":1"
|
||||
result = subprocess.run(
|
||||
[
|
||||
"bash",
|
||||
"-c",
|
||||
'set -u; [ -n "${XDG_SESSION_TYPE:-}" ] && echo xdg; '
|
||||
'[ -n "${DISPLAY:-}" ] && echo x11',
|
||||
],
|
||||
check=False,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert result.stdout.strip() == "x11"
|
||||
|
||||
|
||||
def test_agents_does_not_claim_requirements_are_consumed() -> None:
|
||||
text = AGENTS.read_text(encoding="utf-8")
|
||||
assert "are unused until those phases land" in text
|
||||
assert "consumed by `install.sh`" not in text
|
||||
assert "https://just.systems" in text
|
||||
Reference in New Issue
Block a user