fix(install): leftover review notes from #700 and #705 (#706)

* 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:
Jatin K Malik
2026-08-18 17:41:37 -07:00
committed by GitHub
parent 95d135ebbf
commit 994dda10a6
7 changed files with 338 additions and 72 deletions
+9 -6
View File
@@ -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
+4 -2
View File
@@ -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
View File
@@ -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`.
+3 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
+175
View File
@@ -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