refactor(installer): generate distro package map (#872)

* refactor(installer): generate distro package map

* fix(installer): handle Debian derivative package maps

* docs(installer): document generated package map

* fix(installer): validate package map and refresh derivative probes

* fix(installer): name openSUSE package roles
This commit is contained in:
Sergey Silaev
2026-09-29 21:16:03 -07:00
committed by GitHub
parent fb9166ed9e
commit c0217a8aec
19 changed files with 1057 additions and 465 deletions
+4 -1
View File
@@ -82,6 +82,8 @@ just deps # sync .venv with dev+vad extras and the lint group
just deps-all # also whisper/vosk/docs; later recipes use --no-sync so they keep it
just lock # regenerate uv.lock, requirements/*.txt and the Flatpak dep manifest
just lock-check # fail if uv.lock is stale vs pyproject.toml
just distro-packages # regenerate install.d/package_map.sh from YAML
just distro-packages-check # fail if the generated package map is stale
just flatpak-deps # regenerate the Flatpak dep manifest from requirements/runtime.txt (needs PyPI)
just flatpak-deps-check # fail if that manifest is behind the export
just model-checksums # refresh pinned model digests after adding a model
@@ -164,7 +166,8 @@ src/vocalinux/
Also: `tests/`, `docs/`, `packaging/` (AppImage, AUR, Flatpak), `scripts/`,
`install.sh` (bootstrap and orchestration), `install.d/` (sourced installer modules),
`uninstall.sh`, `web/`.
`uninstall.sh`, `web/`. `scripts/distro-package-map.yaml` owns installer package
names; `install.d/package_map.sh` is generated by `just distro-packages`.
| Task | Start here |
|---|---|
+26 -3
View File
@@ -37,21 +37,24 @@ This installs system dependencies, creates a venv from the system Python, instal
```bash
sudo apt update
sudo apt install -y python3-pip python3-gi python3-gi-cairo \
gir1.2-gtk-3.0 libgirepository-2.0-dev libgirepository1.0-dev \
gir1.2-gtk-3.0 gir1.2-ibus-1.0 libgirepository-2.0-dev libgirepository1.0-dev \
build-essential pkg-config libcairo2-dev \
python3-dev portaudio19-dev python3-venv xdotool
```
**Debian 12** cannot pip-build PyGObject 3.56 (glib 2.74). Use `./install.sh --dev` for tests and running from source, then `venv/bin/pytest` / `venv/bin/python -m vocalinux.main --debug`.
```bash
sudo apt install -y python3-pip python3-gi python3-gi-cairo \
gir1.2-gtk-3.0 libgirepository1.0-dev libcairo2-dev \
gir1.2-gtk-3.0 gir1.2-ibus-1.0 libgirepository1.0-dev libcairo2-dev \
build-essential pkg-config \
python3-dev portaudio19-dev python3-venv xdotool
```
**Debian 13+:**
```bash
sudo apt install -y python3-pip python3-gi python3-gi-cairo \
gir1.2-gtk-3.0 libgirepository-2.0-dev libcairo2-dev \
gir1.2-gtk-3.0 gir1.2-ibus-1.0 libgirepository-2.0-dev libcairo2-dev \
build-essential pkg-config \
python3-dev portaudio19-dev python3-venv xdotool
```
@@ -138,7 +141,9 @@ vocalinux/
├── tests/
├── resources/ # Icons and sounds
├── docs/
├── install.d/ # Installer modules, including generated package data
├── packaging/ # AppImage, AUR, Flatpak
├── scripts/ # Maintenance tools and distro package-map source
├── snap/ # Snap recipe
└── web/ # Marketing site (Next.js)
```
@@ -150,6 +155,24 @@ vocalinux/
| Recognition engines | `src/vocalinux/speech_recognition/recognition_manager.py` |
| Text injection | `src/vocalinux/text_injection/text_injector.py` |
| Settings / config | `src/vocalinux/ui/config_manager.py`, `settings_dialog.py` |
| Installer package names | `scripts/distro-package-map.yaml` |
### Installer package map
`scripts/distro-package-map.yaml` is the source of truth for package names,
alternatives, and conditional package probes used by `install.sh`. Do not edit
the generated `install.d/package_map.sh` directly. After changing the YAML, run:
```bash
just distro-packages
just distro-packages-check
```
Commit both the YAML and generated shell module. Package-manager control flow
and installation policy remain handwritten in
`install.d/system_dependencies.sh`; user systems never parse the YAML. The
remote bootstrap's pre-checkout `git` installation is the deliberate exception
because the generated module is not available yet.
## Testing
+22 -11
View File
@@ -36,9 +36,10 @@ The `.desktop` entry is automatically configured with the detected `GI_TYPELIB_P
## Officially Supported Distributions
These distributions are tested and known to work well with Vocalinux. A
derivative is judged by the Python it ships rather than by its own version
number, which rarely tracks the base release, and `install.sh` checks the
interpreter rather than the release label.
derivative is judged by the Python and package repositories it ships rather
than by its own version number, which rarely tracks the base release. In
particular, `install.sh` does not interpret a Debian-based derivative's product
version as a Debian release number.
| Distribution | Version | Status | Notes |
|--------------|---------|--------|-------|
@@ -72,13 +73,20 @@ These distributions are known to be incompatible:
| Distribution | Status | Reason |
|--------------|--------|--------|
| Debian 11 and earlier | ❌ Not Supported | EOL and below the Python 3.11 floor; the installer requires Debian 12+ |
| NixOS | ❌ Not Supported | Completely different filesystem layout (/nix/store), incompatible with standard installer |
## Requirements by Distribution
These are baseline commands for manual installations. The exact inventory used
by `install.sh`, including package alternatives and conditional dependencies,
lives in [`scripts/distro-package-map.yaml`](../scripts/distro-package-map.yaml).
For the complete manual setup sequence, including native build prerequisites,
use [INSTALL_MANUAL.md](INSTALL_MANUAL.md#system-packages-by-distribution).
### Ubuntu/Debian-based
```bash
sudo apt install -y python3-gi gir1.2-gtk-3.0 gir1.2-gdkpixbuf-2.0 \
sudo apt install -y python3-gi gir1.2-gtk-3.0 gir1.2-ibus-1.0 gir1.2-gdkpixbuf-2.0 \
portaudio19-dev python3-dev python3-venv pkg-config
```
@@ -134,13 +142,13 @@ the VOSK engine (`--engine=vosk`) as a fully-packaged alternative.
### Fedora/RHEL-based
```bash
sudo dnf install -y python3-gobject gtk3-devel gobject-introspection-devel \
sudo dnf install -y python3-gobject gtk3-devel ibus-devel cairo-devel gobject-introspection-devel \
portaudio-devel python3-devel python3-virtualenv pkg-config
```
### Arch Linux-based
```bash
sudo pacman -S --needed python-gobject gtk3 gobject-introspection \
sudo pacman -S --needed python-gobject gtk3 ibus gobject-introspection \
portaudio python pkg-config
```
@@ -151,9 +159,11 @@ PYVER=$(python3 -c 'import sys; print(f"python{sys.version_info.major}{sys.versi
sudo zypper install -y \
"${PYVER}-pip" "${PYVER}-gobject" "${PYVER}-gobject-cairo" \
"${PYVER}-devel" "${PYVER}-virtualenv" \
gtk3 typelib-1_0-AyatanaAppIndicator3-0_1 libayatana-appindicator3-1 \
gtk3 ibus typelib-1_0-IBus-1_0 \
typelib-1_0-AyatanaAppIndicator3-0_1 libayatana-appindicator3-1 \
typelib-1_0-Notify-0_7 libnotify4 \
gobject-introspection-devel portaudio-devel pkg-config cmake wget curl unzip \
gcc gcc-c++ make patchelf \
xdotool wtype
# Optional: only needed for whisper.cpp Vulkan GPU builds
@@ -224,7 +234,7 @@ Before running `pip install vocalinux`, `pip install "vocalinux[vosk]"`, or
#### Ubuntu/Debian
```bash
sudo apt install python3-gi gir1.2-gtk-3.0 gir1.2-ayatanaappindicator3-0.1 \
sudo apt install python3-gi gir1.2-gtk-3.0 gir1.2-ibus-1.0 gir1.2-ayatanaappindicator3-0.1 \
libgirepository1.0-dev libcairo2-dev portaudio19-dev python3-dev \
python3-venv pkg-config xdotool wtype wl-clipboard xclip xsel
```
@@ -234,14 +244,14 @@ On Ubuntu 24.04+ or Pop!_OS, install `libgirepository-2.0-dev` if
#### Fedora
```bash
sudo dnf install python3-gobject gtk3 gtk3-devel libayatana-appindicator-gtk3 \
sudo dnf install python3-gobject gtk3 gtk3-devel ibus-devel cairo-devel libayatana-appindicator-gtk3 \
gobject-introspection-devel portaudio-devel python3-devel \
python3-virtualenv pkg-config xdotool wtype wl-clipboard xclip xsel
```
#### Arch Linux
```bash
sudo pacman -S python-gobject gtk3 libayatana-appindicator gobject-introspection \
sudo pacman -S python-gobject gtk3 ibus libayatana-appindicator gobject-introspection \
python-cairo portaudio python-virtualenv pkg-config xdotool wtype wl-clipboard xclip xsel
```
@@ -249,7 +259,8 @@ sudo pacman -S python-gobject gtk3 libayatana-appindicator gobject-introspection
```bash
PYVER=$(python3 -c 'import sys; print(f"python{sys.version_info.major}{sys.version_info.minor}")')
sudo zypper install "${PYVER}-gobject" "${PYVER}-gobject-cairo" gtk3 \
sudo zypper install "${PYVER}-gobject" "${PYVER}-gobject-cairo" gtk3 ibus typelib-1_0-IBus-1_0 \
gcc gcc-c++ make patchelf \
typelib-1_0-AyatanaAppIndicator3-0_1 libayatana-appindicator3-1 \
typelib-1_0-Notify-0_7 libnotify4 \
portaudio-devel "${PYVER}-devel" "${PYVER}-virtualenv" pkg-config xdotool wtype wl-clipboard xclip xsel
+1 -1
View File
@@ -87,7 +87,7 @@ chmod +x Vocalinux-*-x86_64.AppImage # or aarch64
./Vocalinux-*-x86_64.AppImage
```
Built against glibc 2.35, so it starts on Debian 12+, Ubuntu 22.04+, Fedora 36+, Arch, and Tumbleweed. Older bases (RHEL 9, Debian 11, Ubuntu 20.04) are below that floor; use the installer or PyPI there.
Built against glibc 2.35, so it starts on Debian 12+, Ubuntu 22.04+, Fedora 36+, Arch, and Tumbleweed. Older bases (RHEL 9, Debian 11, Ubuntu 20.04) are below that AppImage floor. The installer and PyPI package still require a distro that ships Python 3.11+, so they are not a workaround for Debian 11 or Ubuntu 20.04.
Still needs host text-injection tools (`xdotool` on X11; `wtype` / `ydotool` / clipboard tools on Wayland). Current AppImages rebuild whisper.cpp with Vulkan and use the host GPU driver (`vulkaninfo --summary`). Prefer the installer when you want system deps, a CUDA build, and models set up automatically.
+24 -8
View File
@@ -2,12 +2,21 @@
Use this when the [recommended installer](INSTALL.md) is not a fit, or you want an explicit package list. Prefer `./install.sh` for a normal desktop install.
The commands below are human-readable baselines for manual installs. The
installer's authoritative package inventory, including build helpers and
fallbacks, is [`scripts/distro-package-map.yaml`](../scripts/distro-package-map.yaml).
Its generated shell module is committed so installations do not need a YAML
parser.
Related: [DISTRO_COMPATIBILITY.md](DISTRO_COMPATIBILITY.md), [TROUBLESHOOTING.md](TROUBLESHOOTING.md).
## System packages by distribution
Install desktop/system dependencies **before** creating a venv or running `pip install vocalinux`. Pip cannot install GTK typelibs, AppIndicator, PortAudio, or text-injection tools.
Debian 11 and earlier are unsupported; the Debian instructions start at Debian
12, which is the oldest release with the required Python 3.11.
### Ubuntu
```bash
@@ -15,8 +24,9 @@ sudo apt update
sudo apt install -y \
python3-pip python3-venv python3-dev \
python3-gi python3-gi-cairo \
gir1.2-gtk-3.0 \
libgirepository1.0-dev portaudio19-dev \
gir1.2-gtk-3.0 gir1.2-ibus-1.0 \
libgirepository1.0-dev libcairo2-dev portaudio19-dev \
build-essential pkg-config cmake libssl-dev autoconf automake libtool patchelf \
wget curl unzip
# Tray: older Ubuntu → gir1.2-appindicator3-0.1
@@ -36,8 +46,9 @@ sudo apt update
sudo apt install -y \
python3-pip python3-venv python3-dev \
python3-gi python3-gi-cairo \
gir1.2-gtk-3.0 \
gir1.2-gtk-3.0 gir1.2-ibus-1.0 \
libgirepository1.0-dev libcairo2-dev portaudio19-dev \
build-essential pkg-config cmake libssl-dev autoconf automake libtool patchelf \
wget curl unzip \
gir1.2-ayatanaappindicator3-0.1 \
xdotool wtype wl-clipboard xclip xsel
@@ -50,8 +61,9 @@ sudo apt update
sudo apt install -y \
python3-pip python3-venv python3-dev \
python3-gi python3-gi-cairo \
gir1.2-gtk-3.0 \
gir1.2-gtk-3.0 gir1.2-ibus-1.0 \
libgirepository-2.0-dev libcairo2-dev portaudio19-dev \
build-essential pkg-config cmake libssl-dev autoconf automake libtool patchelf \
wget curl unzip \
gir1.2-ayatanaappindicator3-0.1 \
xdotool wtype wl-clipboard xclip xsel
@@ -64,8 +76,9 @@ Debian notes (OpenSSL build deps, ydotool, etc.): [DISTRO_COMPATIBILITY.md](DIST
```bash
sudo dnf install -y \
python3-pip python3-devel python3-virtualenv \
python3-gobject gtk3 libayatana-appindicator-gtk3 \
gobject-introspection-devel portaudio-devel \
python3-gobject gtk3 ibus-devel libayatana-appindicator-gtk3 \
gobject-introspection-devel portaudio-devel cairo-devel \
gcc gcc-c++ make pkg-config cmake patchelf \
wget curl unzip xdotool wtype wl-clipboard xclip xsel
```
@@ -75,9 +88,10 @@ Prefer the AUR package (`yay -S vocalinux`). Manual packages:
```bash
sudo pacman -S --needed \
python-pip python-gobject gtk3 \
python-pip python-gobject gtk3 ibus \
libayatana-appindicator gobject-introspection \
python-cairo portaudio python-virtualenv \
base-devel pkg-config cmake patchelf \
wget curl unzip xdotool wtype wl-clipboard xclip xsel
```
@@ -89,9 +103,11 @@ PYVER=$(python3 -c 'import sys; print(f"python{sys.version_info.major}{sys.versi
sudo zypper install -y \
"${PYVER}-pip" "${PYVER}-gobject" "${PYVER}-gobject-cairo" \
"${PYVER}-devel" "${PYVER}-virtualenv" \
gtk3 typelib-1_0-AyatanaAppIndicator3-0_1 libayatana-appindicator3-1 \
gtk3 ibus typelib-1_0-IBus-1_0 \
typelib-1_0-AyatanaAppIndicator3-0_1 libayatana-appindicator3-1 \
typelib-1_0-Notify-0_7 libnotify4 \
gobject-introspection-devel portaudio-devel pkg-config cmake \
gcc gcc-c++ make patchelf \
wget curl unzip xdotool wtype wl-clipboard xclip xsel
# Optional: whisper.cpp Vulkan builds
+129
View File
@@ -0,0 +1,129 @@
#!/bin/bash
# Generated by scripts/generate_distro_package_map.py -- do not edit.
# Regenerate with `just distro-packages` after editing
# scripts/distro-package-map.yaml.
DEBIAN_13_PLUS_PROBE_PACKAGE=libgirepository-2.0-dev
load_distro_package_map() {
local package_map_key="$1"
SYSTEM_PACKAGES=()
APPINDICATOR_PACKAGES=()
GI_DEVELOPMENT_PACKAGES=()
OPTIONAL_SYSTEM_PACKAGES=()
PYTHON_PIP_SUFFIX=""
PYTHON_GOBJECT_SUFFIX=""
PYTHON_GOBJECT_CAIRO_SUFFIX=""
PYTHON_DEVEL_SUFFIX=""
PYTHON_VIRTUALENV_SUFFIX=""
PYTHON_VENV_SUFFIX=""
SHADER_COMPILER_PACKAGES=()
VULKAN_PACKAGES=()
XDOTOOL_PACKAGES=()
WTYPE_PACKAGES=()
YDOTOOL_PACKAGES=()
case "$package_map_key" in
ubuntu)
SYSTEM_PACKAGES=(python3-pip python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-ibus-1.0 libcairo2-dev cmake python3-dev build-essential portaudio19-dev python3-venv pkg-config wget curl unzip vulkan-tools libvulkan-dev xclip xsel wl-clipboard libssl-dev autoconf automake libtool patchelf)
APPINDICATOR_PACKAGES=(gir1.2-appindicator3-0.1 gir1.2-ayatanaappindicator3-0.1)
GI_DEVELOPMENT_PACKAGES=(libgirepository-2.0-dev libgirepository1.0-dev)
OPTIONAL_SYSTEM_PACKAGES=(util-linux-extra)
SHADER_COMPILER_PACKAGES=(glslc glslang-tools)
XDOTOOL_PACKAGES=(xdotool)
WTYPE_PACKAGES=(wtype)
YDOTOOL_PACKAGES=(ydotool)
;;
debian_12)
SYSTEM_PACKAGES=(python3-pip python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-ibus-1.0 libcairo2-dev cmake python3-dev build-essential portaudio19-dev python3-venv pkg-config wget curl unzip vulkan-tools libvulkan-dev xclip xsel wl-clipboard libssl-dev autoconf automake libtool patchelf libgirepository1.0-dev gir1.2-ayatanaappindicator3-0.1)
OPTIONAL_SYSTEM_PACKAGES=(util-linux-extra)
SHADER_COMPILER_PACKAGES=(glslc glslang-tools)
XDOTOOL_PACKAGES=(xdotool)
WTYPE_PACKAGES=(wtype)
YDOTOOL_PACKAGES=(ydotool)
;;
debian_13_plus)
SYSTEM_PACKAGES=(python3-pip python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-ibus-1.0 libcairo2-dev cmake python3-dev build-essential portaudio19-dev python3-venv pkg-config wget curl unzip vulkan-tools libvulkan-dev xclip xsel wl-clipboard libssl-dev autoconf automake libtool patchelf libgirepository-2.0-dev gir1.2-ayatanaappindicator3-0.1)
OPTIONAL_SYSTEM_PACKAGES=(util-linux-extra)
SHADER_COMPILER_PACKAGES=(glslc glslang-tools)
XDOTOOL_PACKAGES=(xdotool)
WTYPE_PACKAGES=(wtype)
YDOTOOL_PACKAGES=(ydotool)
;;
fedora)
SYSTEM_PACKAGES=(python3-pip python3-gobject gtk3 ibus-devel gobject-introspection-devel python3-devel portaudio-devel cairo-devel python3-virtualenv pkg-config cmake wget curl unzip vulkan-tools vulkan-loader-devel glslc patchelf xclip xsel wl-clipboard)
APPINDICATOR_PACKAGES=(libayatana-appindicator-gtk3 libappindicator-gtk3)
XDOTOOL_PACKAGES=(xdotool)
WTYPE_PACKAGES=(wtype)
YDOTOOL_PACKAGES=(ydotool)
;;
arch)
SYSTEM_PACKAGES=(python-pip python-gobject gtk3 ibus gobject-introspection python-cairo portaudio python-virtualenv pkg-config cmake wget curl unzip base-devel vulkan-tools vulkan-headers shaderc patchelf xclip xsel wl-clipboard)
APPINDICATOR_PACKAGES=(libayatana-appindicator libappindicator-gtk3)
XDOTOOL_PACKAGES=(xdotool)
WTYPE_PACKAGES=(wtype)
YDOTOOL_PACKAGES=(ydotool)
;;
suse)
SYSTEM_PACKAGES=(gtk3 ibus typelib-1_0-IBus-1_0 gobject-introspection-devel portaudio-devel pkg-config cmake gcc gcc-c++ make wget curl unzip xclip xsel wl-clipboard typelib-1_0-Notify-0_7 libnotify4 patchelf)
APPINDICATOR_PACKAGES=(typelib-1_0-AyatanaAppIndicator3-0_1 typelib-1_0-AppIndicator3-0_1 typelib-1_0-AyatanaAppIndicator-0_1 libayatana-appindicator3-1 libappindicator3-1 libappindicator-gtk3)
SHADER_COMPILER_PACKAGES=(shaderc glslang-devel glslang)
VULKAN_PACKAGES=(vulkan-tools vulkan-devel)
PYTHON_DEVEL_SUFFIX=devel
PYTHON_GOBJECT_SUFFIX=gobject
PYTHON_GOBJECT_CAIRO_SUFFIX=gobject-cairo
PYTHON_PIP_SUFFIX=pip
PYTHON_VENV_SUFFIX=venv
PYTHON_VIRTUALENV_SUFFIX=virtualenv
XDOTOOL_PACKAGES=(xdotool)
WTYPE_PACKAGES=(wtype)
YDOTOOL_PACKAGES=(ydotool)
;;
gentoo)
SYSTEM_PACKAGES=(dev-python/pygobject:3 x11-libs/gtk+:3 dev-libs/libayatana-appindicator media-libs/portaudio dev-lang/python:3.11 pkgconf cmake media-libs/shaderc dev-util/patchelf x11-misc/xclip x11-misc/xsel gui-apps/wl-clipboard)
XDOTOOL_PACKAGES=(x11-misc/xdotool)
WTYPE_PACKAGES=(x11-misc/wtype)
YDOTOOL_PACKAGES=(app-misc/ydotool)
;;
alpine)
SYSTEM_PACKAGES=(py3-gobject3 py3-pip gtk+3.0 py3-cairo portaudio-dev py3-virtualenv pkgconf cmake wget curl unzip shaderc patchelf vulkan-tools xclip xsel wl-clipboard)
XDOTOOL_PACKAGES=(xdotool)
WTYPE_PACKAGES=(wtype)
YDOTOOL_PACKAGES=(ydotool)
;;
void)
SYSTEM_PACKAGES=(python3-pip python3-gobject gtk+3 libappindicator-gtk3 gobject-introspection portaudio-devel python3-devel pkg-config cmake wget curl unzip shaderc patchelf Vulkan-Tools xclip xsel wl-clipboard)
XDOTOOL_PACKAGES=(xdotool)
WTYPE_PACKAGES=(wtype)
YDOTOOL_PACKAGES=(ydotool)
;;
solus)
SYSTEM_PACKAGES=(python3-pip python3-gobject gtk3 libappindicator gobject-introspection-devel portaudio-devel python3-virtualenv pkg-config cmake wget curl unzip shaderc patchelf vulkan-tools xclip xsel wl-clipboard)
XDOTOOL_PACKAGES=(xdotool)
WTYPE_PACKAGES=(wtype)
YDOTOOL_PACKAGES=(ydotool)
;;
mageia)
SYSTEM_PACKAGES=(python3-pip python3-gobject gtk3 ibus-devel gobject-introspection-devel python3-devel portaudio-devel cairo-devel python3-virtualenv pkg-config cmake wget curl unzip vulkan-tools vulkan-loader-devel glslc patchelf xclip xsel wl-clipboard)
XDOTOOL_PACKAGES=(xdotool)
WTYPE_PACKAGES=(wtype)
YDOTOOL_PACKAGES=(ydotool)
;;
*)
echo "Unknown distro package map key: $package_map_key" >&2
return 1
;;
esac
}
+164 -153
View File
@@ -121,15 +121,6 @@ PY
}
suse_install_appindicator_runtime() {
local APPINDICATOR_PACKAGES=(
"typelib-1_0-AyatanaAppIndicator3-0_1"
"typelib-1_0-AppIndicator3-0_1"
"typelib-1_0-AyatanaAppIndicator-0_1"
"libayatana-appindicator3-1"
"libappindicator3-1"
"libappindicator-gtk3"
)
if suse_appindicator_gi_available; then
print_info "AppIndicator/Ayatana GI namespace is already available."
return 0
@@ -161,63 +152,89 @@ suse_shader_compiler_available() {
command_exists glslc || command_exists glslangValidator
}
resolve_debian_package_map_key() {
local DEBIAN_MAJOR="${DISTRO_VERSION%%.*}"
# Only Debian's own VERSION_ID identifies a Debian release. Derivatives
# often use an unrelated product version, so select their compatible map
# by probing the package inventory instead.
if [[ "$DISTRO_ID" == "debian" && "$DEBIAN_MAJOR" =~ ^[0-9]+$ ]]; then
if [ "$DEBIAN_MAJOR" -lt 12 ]; then
print_error "Debian 12 or newer is required (detected Debian $DEBIAN_MAJOR)." >&2
return "$EXIT_MISSING_DEPS"
elif [ "$DEBIAN_MAJOR" -ge 13 ]; then
echo "debian_13_plus"
else
echo "debian_12"
fi
else
# A missing cache is not evidence of an older Debian base. Refresh
# before probing; keep command substitution's stdout for the key only.
sudo apt update >&2 || {
print_error "Failed to refresh package indexes for Debian base detection." >&2
return "$EXIT_NETWORK"
}
if apt-cache show "$DEBIAN_13_PLUS_PROBE_PACKAGE" &>/dev/null 2>&1; then
echo "debian_13_plus"
else
echo "debian_12"
fi
fi
}
# Function to install system dependencies based on the detected distribution
install_system_dependencies() {
print_info "Installing system dependencies..."
# Determine which Vulkan shader package is available (glslc for Ubuntu 24.04+, glslang-tools for 22.04)
local VULKAN_SHADER_PKG="glslang-tools" # Default fallback
if apt-cache show glslc &>/dev/null 2>&1; then
VULKAN_SHADER_PKG="glslc"
local PACKAGE_MAP_KEY="$DISTRO_FAMILY"
if [[ "$DISTRO_FAMILY" == "debian" ]]; then
PACKAGE_MAP_KEY=$(resolve_debian_package_map_key) || exit "$?"
fi
case "$PACKAGE_MAP_KEY" in
ubuntu|debian_12|debian_13_plus|fedora|arch|suse|gentoo|alpine|void|solus|mageia)
load_distro_package_map "$PACKAGE_MAP_KEY" || exit "$EXIT_MISSING_DEPS"
;;
esac
# Define package names for different distributions
# GObject Introspection / GLib headers for building PyGObject and friends.
# Prefer libgirepository-2.0-dev when available (Ubuntu 24.04+, Pop!_OS Cosmic+,
# Debian 13+). When both 1.0 and 2.0 packages exist, install both: 2.0 provides
# the modern GLib GI headers that pip builds need, while 1.0 still pulls
# gobject-introspection tooling. Older distros that only ship 1.0 keep that.
# See #571 (installer previously kept 1.0 whenever apt-cache still listed it).
local GI_DEV_PKG="libgirepository1.0-dev"
if apt-cache show libgirepository-2.0-dev &>/dev/null 2>&1; then
if apt-cache show libgirepository1.0-dev &>/dev/null 2>&1; then
GI_DEV_PKG="libgirepository-2.0-dev libgirepository1.0-dev"
else
GI_DEV_PKG="libgirepository-2.0-dev"
if [[ "$DISTRO_FAMILY" == "ubuntu" ]]; then
local GI_DEV_PACKAGE
local GI_DEV_FOUND="no"
for GI_DEV_PACKAGE in "${GI_DEVELOPMENT_PACKAGES[@]}"; do
if apt-cache show "$GI_DEV_PACKAGE" &>/dev/null 2>&1; then
SYSTEM_PACKAGES+=("$GI_DEV_PACKAGE")
GI_DEV_FOUND="yes"
fi
done
if [[ "$GI_DEV_FOUND" == "no" ]]; then
SYSTEM_PACKAGES+=("${GI_DEVELOPMENT_PACKAGES[1]}")
fi
SYSTEM_PACKAGES+=("${APPINDICATOR_PACKAGES[0]}")
fi
# libssl-dev, autoconf, automake, libtool, patchelf are required for pywhispercpp source
# builds on Debian. On Ubuntu these are typically pulled in transitively, but on a clean
# Debian install they are absent and cause CMake's bootstrap to fail (Hurdle 2 from
# https://medium.com/@cslev/talking-to-my-linux-box-without-talking-to-the-cloud-vocalinux-on-debian-without-the-tears-10bf053ea21b).
local PYWHISPERCPP_BUILD_DEPS="libssl-dev autoconf automake libtool patchelf"
local APT_PACKAGES_UBUNTU="python3-pip python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-appindicator3-0.1 gir1.2-ibus-1.0 $GI_DEV_PKG libcairo2-dev cmake python3-dev build-essential portaudio19-dev python3-venv pkg-config wget curl unzip vulkan-tools libvulkan-dev $VULKAN_SHADER_PKG xclip xsel wl-clipboard $PYWHISPERCPP_BUILD_DEPS"
local APT_PACKAGES_DEBIAN_BASE="python3-pip python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-ibus-1.0 libcairo2-dev cmake python3-dev build-essential portaudio19-dev python3-venv pkg-config wget curl unzip vulkan-tools libvulkan-dev $VULKAN_SHADER_PKG xclip xsel wl-clipboard $PYWHISPERCPP_BUILD_DEPS"
local APT_PACKAGES_DEBIAN_11_12="$APT_PACKAGES_DEBIAN_BASE libgirepository1.0-dev gir1.2-ayatanaappindicator3-0.1"
local APT_PACKAGES_DEBIAN_13_PLUS="$APT_PACKAGES_DEBIAN_BASE libgirepository-2.0-dev gir1.2-ayatanaappindicator3-0.1"
# cairo-devel: the pinned pycairo builds from source; Fedora splits its headers/.pc out.
local DNF_PACKAGES="python3-pip python3-gobject gtk3 ibus-devel gobject-introspection-devel python3-devel portaudio-devel cairo-devel python3-virtualenv pkg-config cmake wget curl unzip vulkan-tools vulkan-loader-devel glslc patchelf xclip xsel wl-clipboard"
local PACMAN_PACKAGES="python-pip python-gobject gtk3 ibus gobject-introspection python-cairo portaudio python-virtualenv pkg-config cmake wget curl unzip base-devel vulkan-tools vulkan-headers shaderc patchelf xclip xsel wl-clipboard"
# ibus + typelib-1_0-IBus-1_0, not ibus-devel: the headers are not needed
# (IBus is reached through GI at runtime), and requiring them pulls gtk-doc,
# which zypper cannot satisfy when awk resolves to busybox-gawk. The runtime
# package is needed: ibus_engine.py spawns `ibus-daemon -x -d -r` and shells
# out to `ibus engine`, and ibus-devel used to pull it in transitively
# (Requires: ibus = %version). The typelib is named as well as implied by
# ibus's own typelib(IBus) require, so the GI dependency stays visible here.
# gcc/gcc-c++/make: the counterpart of build-essential and base-devel above.
# python3-devel supplies headers, not a compiler, and evdev and pyaudio ship
# no wheels, so without these they have nothing to build with.
local ZYPPER_PACKAGES="gtk3 ibus typelib-1_0-IBus-1_0 gobject-introspection-devel portaudio-devel pkg-config cmake gcc gcc-c++ make wget curl unzip xclip xsel wl-clipboard typelib-1_0-Notify-0_7 libnotify4 patchelf"
# Gentoo uses Portage and different package naming convention
local EMERGE_PACKAGES="dev-python/pygobject:3 x11-libs/gtk+:3 dev-libs/libayatana-appindicator media-libs/portaudio dev-lang/python:3.11 pkgconf cmake media-libs/shaderc dev-util/patchelf x11-misc/xclip x11-misc/xsel gui-apps/wl-clipboard"
# Alpine Linux uses apk and has musl libc
local APK_PACKAGES="py3-gobject3 py3-pip gtk+3.0 py3-cairo portaudio-dev py3-virtualenv pkgconf cmake wget curl unzip shaderc patchelf vulkan-tools xclip xsel wl-clipboard"
# Void Linux uses xbps
local XBPS_PACKAGES="python3-pip python3-gobject gtk+3 libappindicator-gtk3 gobject-introspection portaudio-devel python3-devel pkg-config cmake wget curl unzip shaderc patchelf Vulkan-Tools xclip xsel wl-clipboard"
# Solus uses eopkg
local EOPKG_PACKAGES="python3-pip python3-gobject gtk3 libappindicator gobject-introspection-devel portaudio-devel python3-virtualenv pkg-config cmake wget curl unzip shaderc patchelf vulkan-tools xclip xsel wl-clipboard"
if [[ "$DISTRO_FAMILY" == "ubuntu" || "$DISTRO_FAMILY" == "debian" ]]; then
local SHADER_PACKAGE="${SHADER_COMPILER_PACKAGES[1]}"
local SHADER_CANDIDATE
for SHADER_CANDIDATE in "${SHADER_COMPILER_PACKAGES[@]}"; do
if apt-cache show "$SHADER_CANDIDATE" &>/dev/null 2>&1; then
SHADER_PACKAGE="$SHADER_CANDIDATE"
break
fi
done
SYSTEM_PACKAGES+=("$SHADER_PACKAGE")
local OPTIONAL_PACKAGE
for OPTIONAL_PACKAGE in "${OPTIONAL_SYSTEM_PACKAGES[@]}"; do
if apt-cache show "$OPTIONAL_PACKAGE" &>/dev/null 2>&1; then
SYSTEM_PACKAGES+=("$OPTIONAL_PACKAGE")
fi
done
fi
local MISSING_PACKAGES=""
local INSTALL_CMD=""
@@ -225,26 +242,8 @@ install_system_dependencies() {
case "$DISTRO_FAMILY" in
ubuntu|debian)
local APT_PACKAGES="$APT_PACKAGES_UBUNTU"
if [[ "$DISTRO_FAMILY" == "debian" ]]; then
local DEBIAN_MAJOR="${DISTRO_VERSION%%.*}"
if [[ "$DEBIAN_MAJOR" =~ ^[0-9]+$ ]] && [ "$DEBIAN_MAJOR" -ge 13 ]; then
APT_PACKAGES="$APT_PACKAGES_DEBIAN_13_PLUS"
else
APT_PACKAGES="$APT_PACKAGES_DEBIAN_11_12"
fi
fi
# util-linux-extra only exists on newer Ubuntu/Debian (24.04+, Debian 13+).
# On 22.04 those tools ship in the main util-linux package (always installed).
# apt-cache probe handles Ubuntu derivatives (Mint, Zorin) whose VERSION_ID
# does not track the Ubuntu base release.
if apt-cache show util-linux-extra &>/dev/null 2>&1; then
APT_PACKAGES="$APT_PACKAGES util-linux-extra"
fi
# Check for missing packages
for pkg in $APT_PACKAGES; do
for pkg in "${SYSTEM_PACKAGES[@]}"; do
if ! apt_package_installed "$pkg"; then
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
fi
@@ -255,16 +254,16 @@ install_system_dependencies() {
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
FILTERED_PACKAGES=$(echo "$MISSING_PACKAGES" | sed 's/gir1.2-appindicator3-0.1//' | xargs)
if [[ "$DISTRO_FAMILY" == "ubuntu" ]] && echo "$MISSING_PACKAGES" | grep -q "${APPINDICATOR_PACKAGES[0]}"; then
FILTERED_PACKAGES=$(echo "$MISSING_PACKAGES" | sed "s/${APPINDICATOR_PACKAGES[0]}//" | xargs)
if ! DEBIAN_FRONTEND=noninteractive sudo apt install -y gir1.2-appindicator3-0.1 2>/dev/null; then
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)"
if ! DEBIAN_FRONTEND=noninteractive sudo apt install -y "${APPINDICATOR_PACKAGES[0]}" 2>/dev/null; then
print_info "${APPINDICATOR_PACKAGES[0]} not available, trying ${APPINDICATOR_PACKAGES[1]}..."
if ! DEBIAN_FRONTEND=noninteractive sudo apt install -y "${APPINDICATOR_PACKAGES[1]}"; then
print_error "Failed to install appindicator package (tried both ${APPINDICATOR_PACKAGES[*]})"
exit "$EXIT_MISSING_DEPS"
fi
print_info "Successfully installed gir1.2-ayatanaappindicator3-0.1 (modern replacement)"
print_info "Successfully installed ${APPINDICATOR_PACKAGES[1]} (modern replacement)"
fi
if [ -n "$FILTERED_PACKAGES" ]; then
@@ -292,7 +291,7 @@ install_system_dependencies() {
fi
# Check for missing packages
for pkg in $DNF_PACKAGES; do
for pkg in "${SYSTEM_PACKAGES[@]}"; do
if ! dnf_package_installed "$pkg"; then
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
fi
@@ -306,7 +305,7 @@ install_system_dependencies() {
print_info "All required packages are already installed."
fi
install_preferred_appindicator "$INSTALL_CMD" "libayatana-appindicator-gtk3" "libappindicator-gtk3" dnf_package_installed || exit "$EXIT_MISSING_DEPS"
install_preferred_appindicator "$INSTALL_CMD" "${APPINDICATOR_PACKAGES[0]}" "${APPINDICATOR_PACKAGES[1]}" dnf_package_installed || exit "$EXIT_MISSING_DEPS"
;;
arch)
@@ -317,7 +316,7 @@ install_system_dependencies() {
fi
# Check for missing packages
for pkg in $PACMAN_PACKAGES; do
for pkg in "${SYSTEM_PACKAGES[@]}"; do
if ! pacman_package_installed "$pkg"; then
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
fi
@@ -331,7 +330,7 @@ install_system_dependencies() {
print_info "All required packages are already installed."
fi
install_preferred_appindicator "sudo pacman -S --noconfirm" "libayatana-appindicator" "libappindicator-gtk3" pacman_package_installed || exit "$EXIT_MISSING_DEPS"
install_preferred_appindicator "sudo pacman -S --noconfirm" "${APPINDICATOR_PACKAGES[0]}" "${APPINDICATOR_PACKAGES[1]}" pacman_package_installed || exit "$EXIT_MISSING_DEPS"
;;
suse)
@@ -344,11 +343,11 @@ install_system_dependencies() {
sudo zypper refresh || true
if [[ "${SELECTED_ENGINE:-whisper_cpp}" == "whisper_cpp" && "${WHISPERCPP_BACKEND:-}" != "cpu" ]]; then
ZYPPER_PACKAGES="$ZYPPER_PACKAGES vulkan-tools vulkan-devel"
SYSTEM_PACKAGES+=("${VULKAN_PACKAGES[@]}")
fi
local MISSING_ZYPPER_PACKAGES=()
for pkg in $ZYPPER_PACKAGES; do
for pkg in "${SYSTEM_PACKAGES[@]}"; do
if ! suse_package_installed "$pkg"; then
MISSING_ZYPPER_PACKAGES+=("$pkg")
fi
@@ -371,12 +370,12 @@ install_system_dependencies() {
local PY_VIRTUALENV_CANDIDATES=()
local PY_VENV_CANDIDATES=()
read -r -a PY_PIP_CANDIDATES <<< "$(suse_python_package_candidates "pip")"
read -r -a PY_GOBJECT_CANDIDATES <<< "$(suse_python_package_candidates "gobject")"
read -r -a PY_GOBJECT_CAIRO_CANDIDATES <<< "$(suse_python_package_candidates "gobject-cairo")"
read -r -a PY_DEVEL_CANDIDATES <<< "$(suse_python_package_candidates "devel")"
read -r -a PY_VIRTUALENV_CANDIDATES <<< "$(suse_python_package_candidates "virtualenv")"
read -r -a PY_VENV_CANDIDATES <<< "$(suse_python_package_candidates "venv")"
read -r -a PY_PIP_CANDIDATES <<< "$(suse_python_package_candidates "$PYTHON_PIP_SUFFIX")"
read -r -a PY_GOBJECT_CANDIDATES <<< "$(suse_python_package_candidates "$PYTHON_GOBJECT_SUFFIX")"
read -r -a PY_GOBJECT_CAIRO_CANDIDATES <<< "$(suse_python_package_candidates "$PYTHON_GOBJECT_CAIRO_SUFFIX")"
read -r -a PY_DEVEL_CANDIDATES <<< "$(suse_python_package_candidates "$PYTHON_DEVEL_SUFFIX")"
read -r -a PY_VIRTUALENV_CANDIDATES <<< "$(suse_python_package_candidates "$PYTHON_VIRTUALENV_SUFFIX")"
read -r -a PY_VENV_CANDIDATES <<< "$(suse_python_package_candidates "$PYTHON_VENV_SUFFIX")"
print_info "Resolving openSUSE Python packages for $(suse_python_package_prefix)..."
@@ -407,13 +406,13 @@ 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"
print_error "Try manually: sudo zypper install ${APPINDICATOR_PACKAGES[0]} ${APPINDICATOR_PACKAGES[3]}"
exit "$EXIT_MISSING_DEPS"
fi
if [[ "${SELECTED_ENGINE:-whisper_cpp}" == "whisper_cpp" && "${WHISPERCPP_BACKEND:-}" != "cpu" ]]; then
if ! suse_shader_compiler_available; then
if ! suse_install_first_available "Vulkan shader compiler" shaderc glslang-devel glslang; then
if ! suse_install_first_available "Vulkan shader compiler" "${SHADER_COMPILER_PACKAGES[@]}"; then
print_warning "No Vulkan shader compiler found - whisper.cpp Vulkan build may fail"
print_warning "Install shaderc manually for glslc support if you want GPU acceleration."
fi
@@ -437,7 +436,7 @@ install_system_dependencies() {
# Check for missing packages
MISSING_PACKAGES=""
for pkg in $EMERGE_PACKAGES; do
for pkg in "${SYSTEM_PACKAGES[@]}"; do
# Gentoo uses qlist to check if packages are installed
if ! qlist -I "$pkg" >/dev/null 2>&1; then
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
@@ -467,7 +466,7 @@ install_system_dependencies() {
# Check for missing packages
MISSING_PACKAGES=""
for pkg in $APK_PACKAGES; do
for pkg in "${SYSTEM_PACKAGES[@]}"; do
if ! apk info -e "$pkg" >/dev/null 2>&1; then
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
fi
@@ -493,7 +492,7 @@ install_system_dependencies() {
# Check for missing packages
MISSING_PACKAGES=""
for pkg in $XBPS_PACKAGES; do
for pkg in "${SYSTEM_PACKAGES[@]}"; do
if ! xbps-query "$pkg" >/dev/null 2>&1; then
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
fi
@@ -518,7 +517,7 @@ install_system_dependencies() {
# Check for missing packages
MISSING_PACKAGES=""
for pkg in $EOPKG_PACKAGES; do
for pkg in "${SYSTEM_PACKAGES[@]}"; do
if ! eopkg list-installed | grep -qw "$pkg"; then
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
fi
@@ -546,7 +545,7 @@ install_system_dependencies() {
fi
# Use similar packages to Fedora/RHEL
for pkg in $DNF_PACKAGES; do
for pkg in "${SYSTEM_PACKAGES[@]}"; do
# Mageia uses rpm like Fedora
if ! rpm -q "$pkg" >/dev/null 2>&1; then
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
@@ -604,6 +603,18 @@ install_text_input_tools() {
return 0
fi
# Unsupported distros can continue past install_system_dependencies in
# non-interactive mode, but they have no generated package inventory.
if [[ -z "${XDOTOOL_PACKAGES+x}" ]]; then
print_warning "No text input package map is available for $DISTRO_NAME."
print_warning "Install xdotool or wtype manually for your display server."
return 0
fi
local XDOTOOL_PKG="${XDOTOOL_PACKAGES[0]}"
local WTYPE_PKG="${WTYPE_PACKAGES[0]}"
local YDOTOOL_PKG="${YDOTOOL_PACKAGES[0]}"
# Detect session type more robustly
local SESSION_TYPE="unknown"
@@ -633,64 +644,64 @@ install_text_input_tools() {
print_info "Installing Wayland text input tools..."
case "$DISTRO_FAMILY" in
ubuntu|debian)
if ! apt_package_installed "wtype"; then
DEBIAN_FRONTEND=noninteractive sudo apt install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
if ! apt_package_installed "$WTYPE_PKG"; then
DEBIAN_FRONTEND=noninteractive sudo apt install -y "$WTYPE_PKG" || { print_warning "Failed to install wtype. Text injection may not work properly."; }
else
print_info "wtype is already installed."
fi
;;
fedora)
if command_exists dnf && ! dnf_package_installed "wtype"; then
sudo dnf install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
elif command_exists yum && ! rpm -q wtype &>/dev/null; then
sudo yum install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
if command_exists dnf && ! dnf_package_installed "$WTYPE_PKG"; then
sudo dnf install -y "$WTYPE_PKG" || { print_warning "Failed to install wtype. Text injection may not work properly."; }
elif command_exists yum && ! rpm -q "$WTYPE_PKG" &>/dev/null; then
sudo yum install -y "$WTYPE_PKG" || { print_warning "Failed to install wtype. Text injection may not work properly."; }
else
print_info "wtype is already installed."
fi
;;
arch)
if ! pacman_package_installed "wtype"; then
sudo pacman -S --noconfirm wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
if ! pacman_package_installed "$WTYPE_PKG"; then
sudo pacman -S --noconfirm "$WTYPE_PKG" || { print_warning "Failed to install wtype. Text injection may not work properly."; }
else
print_info "wtype is already installed."
fi
;;
suse)
sudo zypper install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
sudo zypper install -y "$WTYPE_PKG" || { print_warning "Failed to install wtype. Text injection may not work properly."; }
;;
gentoo)
if ! qlist -I wtype >/dev/null 2>&1; then
sudo emerge wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
if ! qlist -I "$WTYPE_PKG" >/dev/null 2>&1; then
sudo emerge "$WTYPE_PKG" || { print_warning "Failed to install wtype. Text injection may not work properly."; }
else
print_info "wtype is already installed."
fi
;;
alpine)
if ! apk info -e wtype >/dev/null 2>&1; then
sudo apk add wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
if ! apk info -e "$WTYPE_PKG" >/dev/null 2>&1; then
sudo apk add "$WTYPE_PKG" || { print_warning "Failed to install wtype. Text injection may not work properly."; }
else
print_info "wtype is already installed."
fi
;;
void)
if ! xbps-query wtype >/dev/null 2>&1; then
sudo xbps-install -Sy wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
if ! xbps-query "$WTYPE_PKG" >/dev/null 2>&1; then
sudo xbps-install -Sy "$WTYPE_PKG" || { print_warning "Failed to install wtype. Text injection may not work properly."; }
else
print_info "wtype is already installed."
fi
;;
solus)
if ! eopkg list-installed | grep -qw wtype; then
sudo eopkg install wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
if ! eopkg list-installed | grep -qw "$WTYPE_PKG"; then
sudo eopkg install "$WTYPE_PKG" || { print_warning "Failed to install wtype. Text injection may not work properly."; }
else
print_info "wtype is already installed."
fi
;;
mageia)
if command_exists dnf && ! rpm -q wtype >/dev/null 2>&1; then
sudo dnf install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
elif command_exists urpmi && ! rpm -q wtype >/dev/null 2>&1; then
sudo urpmi -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
if command_exists dnf && ! rpm -q "$WTYPE_PKG" >/dev/null 2>&1; then
sudo dnf install -y "$WTYPE_PKG" || { print_warning "Failed to install wtype. Text injection may not work properly."; }
elif command_exists urpmi && ! rpm -q "$WTYPE_PKG" >/dev/null 2>&1; then
sudo urpmi -y "$WTYPE_PKG" || { print_warning "Failed to install wtype. Text injection may not work properly."; }
else
print_info "wtype is already installed."
fi
@@ -706,8 +717,8 @@ install_text_input_tools() {
print_info "Attempting to install ydotool for better Wayland compatibility..."
case "$DISTRO_FAMILY" in
ubuntu|debian)
if ! apt_package_installed "ydotool"; then
if ! DEBIAN_FRONTEND=noninteractive sudo apt install -y ydotool 2>/dev/null; then
if ! apt_package_installed "$YDOTOOL_PKG"; then
if ! DEBIAN_FRONTEND=noninteractive sudo apt install -y "$YDOTOOL_PKG" 2>/dev/null; then
if [[ "$DISTRO_FAMILY" == "debian" ]]; then
print_warning "ydotool is not packaged in Debian's standard repos."
print_info "For full Wayland input support, you can compile ydotool from source:"
@@ -724,12 +735,12 @@ install_text_input_tools() {
;;
fedora)
if command_exists dnf; then
sudo dnf install -y ydotool 2>/dev/null || print_info "ydotool not available in repos (optional)"
sudo dnf install -y "$YDOTOOL_PKG" 2>/dev/null || print_info "ydotool not available in repos (optional)"
fi
;;
arch)
if ! pacman_package_installed "ydotool"; then
sudo pacman -S --noconfirm ydotool 2>/dev/null || print_info "ydotool not available in repos (optional)"
if ! pacman_package_installed "$YDOTOOL_PKG"; then
sudo pacman -S --noconfirm "$YDOTOOL_PKG" 2>/dev/null || print_info "ydotool not available in repos (optional)"
fi
;;
esac
@@ -755,64 +766,64 @@ install_text_input_tools() {
print_info "Installing X11 text input tools..."
case "$DISTRO_FAMILY" in
ubuntu|debian)
if ! apt_package_installed "xdotool"; then
DEBIAN_FRONTEND=noninteractive sudo apt install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
if ! apt_package_installed "$XDOTOOL_PKG"; then
DEBIAN_FRONTEND=noninteractive sudo apt install -y "$XDOTOOL_PKG" || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
else
print_info "xdotool is already installed."
fi
;;
fedora)
if command_exists dnf && ! dnf_package_installed "xdotool"; then
sudo dnf install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
elif command_exists yum && ! rpm -q xdotool &>/dev/null; then
sudo yum install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
if command_exists dnf && ! dnf_package_installed "$XDOTOOL_PKG"; then
sudo dnf install -y "$XDOTOOL_PKG" || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
elif command_exists yum && ! rpm -q "$XDOTOOL_PKG" &>/dev/null; then
sudo yum install -y "$XDOTOOL_PKG" || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
else
print_info "xdotool is already installed."
fi
;;
arch)
if ! pacman_package_installed "xdotool"; then
sudo pacman -S --noconfirm xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
if ! pacman_package_installed "$XDOTOOL_PKG"; then
sudo pacman -S --noconfirm "$XDOTOOL_PKG" || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
else
print_info "xdotool is already installed."
fi
;;
suse)
sudo zypper install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
sudo zypper install -y "$XDOTOOL_PKG" || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
;;
gentoo)
if ! qlist -I xdotool >/dev/null 2>&1; then
sudo emerge xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
if ! qlist -I "$XDOTOOL_PKG" >/dev/null 2>&1; then
sudo emerge "$XDOTOOL_PKG" || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
else
print_info "xdotool is already installed."
fi
;;
alpine)
if ! apk info -e xdotool >/dev/null 2>&1; then
sudo apk add xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
if ! apk info -e "$XDOTOOL_PKG" >/dev/null 2>&1; then
sudo apk add "$XDOTOOL_PKG" || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
else
print_info "xdotool is already installed."
fi
;;
void)
if ! xbps-query xdotool >/dev/null 2>&1; then
sudo xbps-install -Sy xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
if ! xbps-query "$XDOTOOL_PKG" >/dev/null 2>&1; then
sudo xbps-install -Sy "$XDOTOOL_PKG" || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
else
print_info "xdotool is already installed."
fi
;;
solus)
if ! eopkg list-installed | grep -qw xdotool; then
sudo eopkg install xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
if ! eopkg list-installed | grep -qw "$XDOTOOL_PKG"; then
sudo eopkg install "$XDOTOOL_PKG" || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
else
print_info "xdotool is already installed."
fi
;;
mageia)
if command_exists dnf && ! rpm -q xdotool >/dev/null 2>&1; then
sudo dnf install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
elif command_exists urpmi && ! rpm -q xdotool >/dev/null 2>&1; then
sudo urpmi -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
if command_exists dnf && ! rpm -q "$XDOTOOL_PKG" >/dev/null 2>&1; then
sudo dnf install -y "$XDOTOOL_PKG" || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
elif command_exists urpmi && ! rpm -q "$XDOTOOL_PKG" >/dev/null 2>&1; then
sudo urpmi -y "$XDOTOOL_PKG" || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
else
print_info "xdotool is already installed."
fi
@@ -831,36 +842,36 @@ install_text_input_tools() {
# Install both tools based on distribution
case "$DISTRO_FAMILY" in
ubuntu|debian)
DEBIAN_FRONTEND=noninteractive sudo apt install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
DEBIAN_FRONTEND=noninteractive sudo apt install -y "$XDOTOOL_PKG" "$WTYPE_PKG" || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
;;
fedora|mageia)
if command_exists dnf; then
sudo dnf install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
sudo dnf install -y "$XDOTOOL_PKG" "$WTYPE_PKG" || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
elif command_exists yum; then
sudo yum install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
sudo yum install -y "$XDOTOOL_PKG" "$WTYPE_PKG" || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
fi
# Mageia also supports urpmi
if [[ "$DISTRO_FAMILY" == "mageia" ]] && command_exists urpmi; then
sudo urpmi -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
sudo urpmi -y "$XDOTOOL_PKG" "$WTYPE_PKG" || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
fi
;;
arch)
sudo pacman -S --noconfirm xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
sudo pacman -S --noconfirm "$XDOTOOL_PKG" "$WTYPE_PKG" || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
;;
suse)
sudo zypper install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
sudo zypper install -y "$XDOTOOL_PKG" "$WTYPE_PKG" || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
;;
gentoo)
sudo emerge xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
sudo emerge "$XDOTOOL_PKG" "$WTYPE_PKG" || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
;;
alpine)
sudo apk add xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
sudo apk add "$XDOTOOL_PKG" "$WTYPE_PKG" || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
;;
void)
sudo xbps-install -Sy xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
sudo xbps-install -Sy "$XDOTOOL_PKG" "$WTYPE_PKG" || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
;;
solus)
sudo eopkg install xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
sudo eopkg install "$XDOTOOL_PKG" "$WTYPE_PKG" || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
;;
*)
print_warning "Unsupported distribution for text input tools."
+1 -1
View File
@@ -707,7 +707,7 @@ source_installer_module() {
# Shared by the model installer module and the runtime downloader.
MODEL_CHECKSUMS_FILE="$INSTALL_DIR/src/vocalinux/utils/model_checksums.txt"
for INSTALLER_MODULE in interactive.sh system_dependencies.sh models.sh desktop.sh; do
for INSTALLER_MODULE in package_map.sh interactive.sh system_dependencies.sh models.sh desktop.sh; do
source_installer_module "$INSTALLER_MODULE" || exit 1
done
unset INSTALLER_MODULE
+9
View File
@@ -193,6 +193,15 @@ lock-check:
export-check:
python3 scripts/check_exports.py
# Regenerate the shell package inventory consumed by install.sh. The committed
# output keeps the runtime installer independent of a YAML parser.
distro-packages: _tooling
uv run --no-sync python scripts/generate_distro_package_map.py
# Fail if install.d/package_map.sh is behind its YAML source.
distro-packages-check: _tooling
uv run --no-sync python scripts/generate_distro_package_map.py --check
# The export pins names, versions and digests; this looks up the URL that serves
# those bytes, so it needs PyPI. tests/test_flatpak_packaging.py checks the same
# invariant offline, which is what CI gates on.
+1
View File
@@ -72,6 +72,7 @@ dev = [
"pytest-timeout>=2.1.0",
"pre-commit>=3.0.0",
"mypy>=1.0.0",
"pyyaml>=6.0,<7",
]
docs = [
"sphinx>=6.0.0",
+3 -1
View File
@@ -993,7 +993,9 @@ pyyaml==6.0.3 \
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
# via pre-commit
# via
# pre-commit
# vocalinux
requests==2.34.2 \
--hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
--hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
+178 -242
View File
@@ -1,256 +1,192 @@
# Package name mapping for different Linux distributions
# This file documents the package names used by the installer for each distribution
# It serves as a reference for manual installation and for adding support for new distributions
# Authoritative distro package inventory for install.sh.
#
# Edit this file, then run `just distro-packages`. The installer consumes the
# generated install.d/package_map.sh and never needs a YAML parser at runtime.
# The remote bootstrap's git package is the one deliberate exception: it must
# be installed before the tagged repository and this generated map are present.
schema_version: 1
# System packages required for Vocalinux
system_packages:
# GTK3 and PyGObject
gtk3:
ubuntu: ["python3-gi", "python3-gi-cairo", "gir1.2-gtk-3.0", "gir1.2-gdkpixbuf-2.0"]
debian: ["python3-gi", "python3-gi-cairo", "gir1.2-gtk-3.0", "gir1.2-gdkpixbuf-2.0", "libcairo2-dev"]
fedora: ["python3-gobject", "gtk3", "gobject-introspection-devel"]
arch: ["python-gobject", "gtk3", "gobject-introspection", "python-cairo"]
# Tumbleweed often uses versioned Python package names such as python313-gobject.
# NOTE: "python3XY" is a placeholder convention. The XY suffix is resolved at
# install time by install.sh via suse_python_package_prefix() and
# suse_python_package_candidates(), which try the active Python minor version
# first (e.g. python313-gobject) and fall back to the unversioned name.
opensuse: ["python3XY-gobject", "python3XY-gobject-cairo", "gtk3", "gobject-introspection-devel"]
gentoo: ["dev-python/pygobject:3", "x11-libs/gtk+:3"]
alpine: ["py3-gobject3", "py3-cairo", "gtk+3.0"]
void: ["python3-gobject", "gtk+3"]
solus: ["python3-gobject", "gtk3"]
distributions:
ubuntu:
# libssl/autotools/libtool/patchelf support pywhispercpp source builds.
system:
- python3-pip
- python3-gi
- python3-gi-cairo
- gir1.2-gtk-3.0
- gir1.2-ibus-1.0
- libcairo2-dev
- cmake
- python3-dev
- build-essential
- portaudio19-dev
- python3-venv
- pkg-config
- wget
- curl
- unzip
- vulkan-tools
- libvulkan-dev
- xclip
- xsel
- wl-clipboard
- libssl-dev
- autoconf
- automake
- libtool
- patchelf
appindicator: [gir1.2-appindicator3-0.1, gir1.2-ayatanaappindicator3-0.1]
gi_development: [libgirepository-2.0-dev, libgirepository1.0-dev]
shader_compiler: [glslc, glslang-tools]
optional_system: [util-linux-extra]
text_input:
xdotool: [xdotool]
wtype: [wtype]
ydotool: [ydotool]
# AppIndicator / Ayatana AppIndicator
# Prefer Ayatana: legacy Canonical AppIndicator can load without error but
# fail to register a StatusNotifierItem on modern KDE Plasma.
appindicator:
ubuntu: ["gir1.2-appindicator3-0.1"]
debian_11: ["gir1.2-ayatanaappindicator3-0.1"]
debian_12_plus: ["gir1.2-ayatanaappindicator3-0.1"]
fedora: ["libayatana-appindicator-gtk3"]
arch: ["libayatana-appindicator"]
opensuse: ["typelib-1_0-AyatanaAppIndicator3-0_1", "libayatana-appindicator3-1", "typelib-1_0-AppIndicator3-0_1"]
gentoo: ["dev-libs/libayatana-appindicator"]
alpine: ["libayatana-appindicator"]
void: ["libappindicator"]
solus: ["libappindicator"]
debian_12:
system:
- python3-pip
- python3-gi
- python3-gi-cairo
- gir1.2-gtk-3.0
- gir1.2-ibus-1.0
- libcairo2-dev
- cmake
- python3-dev
- build-essential
- portaudio19-dev
- python3-venv
- pkg-config
- wget
- curl
- unzip
- vulkan-tools
- libvulkan-dev
- xclip
- xsel
- wl-clipboard
- libssl-dev
- autoconf
- automake
- libtool
- patchelf
- libgirepository1.0-dev
- gir1.2-ayatanaappindicator3-0.1
shader_compiler: [glslc, glslang-tools]
optional_system: [util-linux-extra]
text_input:
xdotool: [xdotool]
wtype: [wtype]
ydotool: [ydotool]
# PortAudio
portaudio:
ubuntu: ["portaudio19-dev"]
debian: ["portaudio19-dev"]
fedora: ["portaudio-devel"]
arch: ["portaudio"]
opensuse: ["portaudio-devel"]
gentoo: ["media-libs/portaudio"]
alpine: ["portaudio-dev"]
void: ["portaudio-devel"]
solus: ["portaudio-devel"]
debian_13_plus:
# Used only to distinguish Debian 13-based derivatives whose own
# VERSION_ID does not match the Debian base release.
selection_probe: libgirepository-2.0-dev
system:
- python3-pip
- python3-gi
- python3-gi-cairo
- gir1.2-gtk-3.0
- gir1.2-ibus-1.0
- libcairo2-dev
- cmake
- python3-dev
- build-essential
- portaudio19-dev
- python3-venv
- pkg-config
- wget
- curl
- unzip
- vulkan-tools
- libvulkan-dev
- xclip
- xsel
- wl-clipboard
- libssl-dev
- autoconf
- automake
- libtool
- patchelf
- libgirepository-2.0-dev
- gir1.2-ayatanaappindicator3-0.1
shader_compiler: [glslc, glslang-tools]
optional_system: [util-linux-extra]
text_input:
xdotool: [xdotool]
wtype: [wtype]
ydotool: [ydotool]
# Python development
python_dev:
ubuntu: ["python3-dev", "python3-venv"]
debian: ["python3-dev", "python3-venv"]
fedora: ["python3-devel", "python3-virtualenv"]
arch: ["python"] # Arch includes venv by default
# Tumbleweed package names follow the active Python minor version, e.g. python313-devel.
# NOTE: "python3XY" is a placeholder — see gtk3.opensuse comment above for details.
opensuse: ["python3XY-devel", "python3XY-virtualenv", "python3XY-venv", "python3XY-pip"]
gentoo: ["dev-lang/python:3.11"]
alpine: ["py3-virtualenv"]
void: ["python3-devel"]
solus: ["python3-virtualenv"]
fedora:
# cairo-devel is needed because the pinned pycairo builds from source.
system: [python3-pip, python3-gobject, gtk3, ibus-devel, gobject-introspection-devel, python3-devel, portaudio-devel, cairo-devel, python3-virtualenv, pkg-config, cmake, wget, curl, unzip, vulkan-tools, vulkan-loader-devel, glslc, patchelf, xclip, xsel, wl-clipboard]
appindicator: [libayatana-appindicator-gtk3, libappindicator-gtk3]
text_input:
xdotool: [xdotool]
wtype: [wtype]
ydotool: [ydotool]
# Package build tools
build_tools:
ubuntu: ["pkg-config", "wget", "curl", "unzip"]
debian: ["pkg-config", "wget", "curl", "unzip"]
fedora: ["pkg-config", "wget", "curl", "unzip"]
arch: ["pkgconf", "wget", "curl", "unzip"]
opensuse: ["pkg-config", "wget", "curl", "unzip"]
gentoo: ["dev-util/pkgconfig", "net-misc/wget", "net-misc/curl", "app-arch/unzip"]
alpine: ["pkgconf", "wget", "curl", "unzip"]
void: ["pkg-config", "wget", "curl", "unzip"]
solus: ["pkg-config", "wget", "curl", "unzip"]
arch:
system: [python-pip, python-gobject, gtk3, ibus, gobject-introspection, python-cairo, portaudio, python-virtualenv, pkg-config, cmake, wget, curl, unzip, base-devel, vulkan-tools, vulkan-headers, shaderc, patchelf, xclip, xsel, wl-clipboard]
appindicator: [libayatana-appindicator, libappindicator-gtk3]
text_input:
xdotool: [xdotool]
wtype: [wtype]
ydotool: [ydotool]
# Text injection tools
xdotool:
ubuntu: ["xdotool"]
debian: ["xdotool"]
fedora: ["xdotool"]
arch: ["xdotool"]
opensuse: ["xdotool"]
gentoo: ["x11-misc/xdotool"]
alpine: ["xdotool"]
void: ["xdotool"]
solus: ["xdotool"]
# Clipboard tools used for layout-independent Wayland paste fallback
clipboard_tools:
ubuntu: ["wl-clipboard", "xclip", "xsel"]
debian: ["wl-clipboard", "xclip", "xsel"]
fedora: ["wl-clipboard", "xclip", "xsel"]
arch: ["wl-clipboard", "xclip", "xsel"]
opensuse: ["wl-clipboard", "xclip", "xsel"]
gentoo: ["gui-apps/wl-clipboard", "x11-misc/xclip", "x11-misc/xsel"]
alpine: ["wl-clipboard", "xclip", "xsel"]
void: ["wl-clipboard", "xclip", "xsel"]
solus: ["wl-clipboard", "xclip", "xsel"]
wtype:
ubuntu: ["wtype"]
debian: ["wtype"]
fedora: ["wtype"]
arch: ["wtype"]
opensuse: ["wtype"]
gentoo: ["x11-misc/wtype"]
alpine: ["wtype"]
void: ["wtype"]
solus: ["wtype"]
ydotool:
ubuntu: ["ydotool"]
debian: ["ydotool"]
fedora: ["ydotool"]
arch: ["ydotool"]
opensuse: ["ydotool"]
gentoo: ["app-misc/ydotool"]
alpine: ["ydotool"]
void: ["ydotool"]
solus: ["ydotool"]
# Command templates for package managers
install_commands:
apt: "sudo apt install -y {packages}"
apt_update: "sudo apt update"
dnf: "sudo dnf install -y {packages}"
dnf_update: "sudo dnf update -y" # Optional update
yum: "sudo yum install -y {packages}"
pacman: "sudo pacman -S --noconfirm {packages}"
pacman_update: "sudo pacman -Syu" # Optional update
zypper: "sudo zypper install -y {packages}"
zypper_update: "sudo zypper refresh" # Optional update
emerge: "sudo emerge {packages}"
apk: "sudo apk add {packages}"
apk_update: "sudo apk update" # Optional update
xbps: "sudo xbps-install -Sy {packages}"
xbps_update: "sudo xbps-install -Sy" # Updates sync during install
eopkg: "sudo eopkg install -y {packages}"
eopkg_update: "sudo eopkg upgrade" # Optional update
# Distribution family mappings
# This helps determine which package manager and package names to use
distribution_families:
# Ubuntu family
- id: ubuntu
family: ubuntu
package_manager: apt
variations: [pop, linuxmint, elementary, zorin]
# Debian family
- id: debian
family: debian
package_manager: apt
variations: []
# Fedora family
- id: fedora
family: fedora
package_manager: dnf
variations: [rhel, centos, rocky, almalinux]
# Arch family
- id: arch
family: arch
package_manager: pacman
variations: [manjaro, endeavouros]
# openSUSE family
- id: opensuse
family: suse
package_manager: zypper
variations: []
# Gentoo
- id: gentoo
family: gentoo
package_manager: emerge
variations: []
# Alpine
- id: alpine
family: alpine
package_manager: apk
variations: []
# Void Linux
- id: void
family: void
package_manager: xbps
variations: []
# Solus
- id: solus
family: solus
package_manager: eopkg
variations: []
# Mageia
- id: mageia
family: mageia
package_manager: dnf # Prefers dnf, falls back to urpmi
variations: []
# Notes for specific distributions
distribution_notes:
debian_11:
appindicator: "Use gir1.2-ayatanaappindicator3-0.1 instead of gir1.2-appindicator3-0.1"
suse:
# IBus is a runtime dependency; ibus-devel drags in an unsatisfiable gtk-doc
# chain on Tumbleweed. The compilers build evdev and PyAudio from sdists.
system: [gtk3, ibus, typelib-1_0-IBus-1_0, gobject-introspection-devel, portaudio-devel, pkg-config, cmake, gcc, gcc-c++, make, wget, curl, unzip, xclip, xsel, wl-clipboard, typelib-1_0-Notify-0_7, libnotify4, patchelf]
appindicator: [typelib-1_0-AyatanaAppIndicator3-0_1, typelib-1_0-AppIndicator3-0_1, typelib-1_0-AyatanaAppIndicator-0_1, libayatana-appindicator3-1, libappindicator3-1, libappindicator-gtk3]
python_packages:
pip: pip
gobject: gobject
gobject_cairo: gobject-cairo
devel: devel
virtualenv: virtualenv
venv: venv
vulkan: [vulkan-tools, vulkan-devel]
shader_compiler: [shaderc, glslang-devel, glslang]
text_input:
xdotool: [xdotool]
wtype: [wtype]
ydotool: [ydotool]
gentoo:
general: "Gentoo compiles packages from source. Installation will take longer."
python: "Ensure Python 3.11+ is set as the active version"
system: [dev-python/pygobject:3, x11-libs/gtk+:3, dev-libs/libayatana-appindicator, media-libs/portaudio, dev-lang/python:3.11, pkgconf, cmake, media-libs/shaderc, dev-util/patchelf, x11-misc/xclip, x11-misc/xsel, gui-apps/wl-clipboard]
text_input:
xdotool: [x11-misc/xdotool]
wtype: [x11-misc/wtype]
ydotool: [app-misc/ydotool]
alpine:
general: "Alpine uses musl libc. Some Python packages may not have pre-built wheels."
nixos:
general: "NixOS is not supported due to completely different filesystem layout."
system: [py3-gobject3, py3-pip, gtk+3.0, py3-cairo, portaudio-dev, py3-virtualenv, pkgconf, cmake, wget, curl, unzip, shaderc, patchelf, vulkan-tools, xclip, xsel, wl-clipboard]
text_input:
xdotool: [xdotool]
wtype: [wtype]
ydotool: [ydotool]
void:
general: "Void Linux may use either glibc or musl libc. glibc version recommended."
system: [python3-pip, python3-gobject, gtk+3, libappindicator-gtk3, gobject-introspection, portaudio-devel, python3-devel, pkg-config, cmake, wget, curl, unzip, shaderc, patchelf, Vulkan-Tools, xclip, xsel, wl-clipboard]
text_input:
xdotool: [xdotool]
wtype: [wtype]
ydotool: [ydotool]
# Architecture-specific library paths
library_paths:
typelib:
# Debian/Ubuntu multiarch paths
- /usr/lib/x86_64-linux-gnu/girepository-1.0
- /usr/lib/aarch64-linux-gnu/girepository-1.0
- /usr/lib/arm-linux-gnueabihf/girepository-1.0
- /usr/lib/riscv64-linux-gnu/girepository-1.0
- /usr/lib/powerpc64le-linux-gnu/girepository-1.0
- /usr/lib/s390x-linux-gnu/girepository-1.0
solus:
system: [python3-pip, python3-gobject, gtk3, libappindicator, gobject-introspection-devel, portaudio-devel, python3-virtualenv, pkg-config, cmake, wget, curl, unzip, shaderc, patchelf, vulkan-tools, xclip, xsel, wl-clipboard]
text_input:
xdotool: [xdotool]
wtype: [wtype]
ydotool: [ydotool]
# Fedora/RHEL
- /usr/lib64/girepository-1.0
# Arch, generic
- /usr/lib/girepository-1.0
# Local installations
- /usr/local/lib/girepository-1.0
- /usr/local/lib64/girepository-1.0
models:
- /usr/local/share/vocalinux/models
- /usr/share/vocalinux/models
- /usr/lib64/vocalinux/models
- /usr/lib/vocalinux/models
mageia:
# The existing installer deliberately follows Fedora's package inventory.
system: [python3-pip, python3-gobject, gtk3, ibus-devel, gobject-introspection-devel, python3-devel, portaudio-devel, cairo-devel, python3-virtualenv, pkg-config, cmake, wget, curl, unzip, vulkan-tools, vulkan-loader-devel, glslc, patchelf, xclip, xsel, wl-clipboard]
text_input:
xdotool: [xdotool]
wtype: [wtype]
ydotool: [ydotool]
+246
View File
@@ -0,0 +1,246 @@
#!/usr/bin/env python3
"""Generate the installer's shell package map from its YAML source."""
from __future__ import annotations
import argparse
import shlex
import sys
from pathlib import Path
from typing import Any
import yaml
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "scripts" / "distro-package-map.yaml"
TARGET = ROOT / "install.d" / "package_map.sh"
REQUIRED_DISTRIBUTIONS = {
"alpine",
"arch",
"debian_12",
"debian_13_plus",
"fedora",
"gentoo",
"mageia",
"solus",
"suse",
"ubuntu",
"void",
}
LIST_FIELDS = {
"appindicator",
"gi_development",
"optional_system",
"shader_compiler",
"system",
"vulkan",
}
SCALAR_FIELDS = {"selection_probe"}
TEXT_TOOLS = {"wtype", "xdotool", "ydotool"}
SUSE_PYTHON_ROLES = {"pip", "gobject", "gobject_cairo", "devel", "virtualenv", "venv"}
class UniqueKeyLoader(yaml.SafeLoader):
"""Reject duplicate mapping keys instead of silently dropping inventory."""
def construct_mapping(self, node: yaml.MappingNode, deep: bool = False) -> dict:
self.flatten_mapping(node)
result = {}
for key_node, value_node in node.value:
key = self.construct_object(key_node, deep=deep)
if not isinstance(key, str):
raise ValueError("package map keys must be strings")
if key in result:
raise ValueError(f"duplicate YAML key: {key}")
result[key] = self.construct_object(value_node, deep=deep)
return result
def _package_list(value: Any, location: str, *, required: bool = False) -> list[str]:
"""Validate and return one package list."""
if value is None and not required:
return []
if not isinstance(value, list) or (required and not value):
raise ValueError(f"{location} must be a non-empty list")
if any(not isinstance(item, str) or not item for item in value):
raise ValueError(f"{location} must contain non-empty strings")
if len(value) != len(set(value)):
raise ValueError(f"{location} contains duplicate packages")
return value
def load_map() -> dict[str, dict[str, Any]]:
"""Load and validate the authoritative YAML package map."""
document = yaml.load(SOURCE.read_text(encoding="utf-8"), Loader=UniqueKeyLoader)
if not isinstance(document, dict) or document.get("schema_version") != 1:
raise ValueError("distro package map must use schema_version: 1")
distributions = document.get("distributions")
if not isinstance(distributions, dict):
raise ValueError("distro package map must define a distributions mapping")
names = set(distributions)
if names != REQUIRED_DISTRIBUTIONS:
missing = ", ".join(sorted(REQUIRED_DISTRIBUTIONS - names)) or "none"
extra = ", ".join(sorted(names - REQUIRED_DISTRIBUTIONS)) or "none"
raise ValueError(f"distribution keys differ (missing: {missing}; extra: {extra})")
for distro, config in distributions.items():
if not isinstance(config, dict):
raise ValueError(f"distributions.{distro} must be a mapping")
unknown = set(config) - LIST_FIELDS - SCALAR_FIELDS - {"python_packages", "text_input"}
if unknown:
raise ValueError(f"distributions.{distro} has unknown fields: {sorted(unknown)}")
_package_list(config.get("system"), f"distributions.{distro}.system", required=True)
for field in LIST_FIELDS - {"system"}:
_package_list(config.get(field), f"distributions.{distro}.{field}")
selection_probe = config.get("selection_probe")
if selection_probe is not None and (
distro != "debian_13_plus"
or not isinstance(selection_probe, str)
or not selection_probe
):
raise ValueError("selection_probe must be a non-empty string on debian_13_plus only")
python_packages = config.get("python_packages")
if python_packages is not None:
if distro != "suse" or not isinstance(python_packages, dict):
raise ValueError("python_packages must be a mapping on suse only")
if set(python_packages) != SUSE_PYTHON_ROLES:
raise ValueError(
f"distributions.suse.python_packages must define {sorted(SUSE_PYTHON_ROLES)}"
)
if any(not isinstance(value, str) or not value for value in python_packages.values()):
raise ValueError("distributions.suse.python_packages values must be strings")
text_input = config.get("text_input")
if not isinstance(text_input, dict) or set(text_input) != TEXT_TOOLS:
raise ValueError(f"distributions.{distro}.text_input must define {sorted(TEXT_TOOLS)}")
for tool in TEXT_TOOLS:
_package_list(
text_input.get(tool),
f"distributions.{distro}.text_input.{tool}",
required=True,
)
if len(text_input[tool]) != 1:
raise ValueError(
f"distributions.{distro}.text_input.{tool} needs exactly one package"
)
for distro in ("ubuntu", "fedora", "arch"):
if len(distributions[distro].get("appindicator") or []) != 2:
raise ValueError(f"distributions.{distro}.appindicator must define two fallbacks")
if len(distributions["suse"].get("appindicator") or []) < 4:
raise ValueError("distributions.suse.appindicator must define at least four alternatives")
if not distributions["debian_13_plus"].get("selection_probe"):
raise ValueError("distributions.debian_13_plus.selection_probe is required")
if len(distributions["ubuntu"].get("gi_development") or []) != 2:
raise ValueError("distributions.ubuntu.gi_development must define modern and legacy")
for distro in ("ubuntu", "debian_12", "debian_13_plus"):
if len(distributions[distro].get("shader_compiler") or []) < 2:
raise ValueError(f"distributions.{distro}.shader_compiler needs a fallback")
if "python_packages" not in distributions["suse"]:
raise ValueError("distributions.suse.python_packages is required")
for field in ("vulkan", "shader_compiler"):
_package_list(distributions["suse"].get(field), f"suse.{field}", required=True)
return distributions
def _array_assignment(name: str, values: list[str], indent: str = " ") -> str:
quoted = " ".join(shlex.quote(value) for value in values)
return f"{indent}{name}=({quoted})"
def render(distributions: dict[str, dict[str, Any]]) -> str:
"""Render a deterministic Bash module from the validated map."""
debian_13_probe = distributions["debian_13_plus"]["selection_probe"]
lines = [
"#!/bin/bash",
"# Generated by scripts/generate_distro_package_map.py -- do not edit.",
"# Regenerate with `just distro-packages` after editing",
"# scripts/distro-package-map.yaml.",
"",
f"DEBIAN_13_PLUS_PROBE_PACKAGE={shlex.quote(debian_13_probe)}",
"",
"load_distro_package_map() {",
' local package_map_key="$1"',
"",
" SYSTEM_PACKAGES=()",
" APPINDICATOR_PACKAGES=()",
" GI_DEVELOPMENT_PACKAGES=()",
" OPTIONAL_SYSTEM_PACKAGES=()",
' PYTHON_PIP_SUFFIX=""',
' PYTHON_GOBJECT_SUFFIX=""',
' PYTHON_GOBJECT_CAIRO_SUFFIX=""',
' PYTHON_DEVEL_SUFFIX=""',
' PYTHON_VIRTUALENV_SUFFIX=""',
' PYTHON_VENV_SUFFIX=""',
" SHADER_COMPILER_PACKAGES=()",
" VULKAN_PACKAGES=()",
" XDOTOOL_PACKAGES=()",
" WTYPE_PACKAGES=()",
" YDOTOOL_PACKAGES=()",
"",
' case "$package_map_key" in',
]
names = {
"system": "SYSTEM_PACKAGES",
"appindicator": "APPINDICATOR_PACKAGES",
"gi_development": "GI_DEVELOPMENT_PACKAGES",
"optional_system": "OPTIONAL_SYSTEM_PACKAGES",
"shader_compiler": "SHADER_COMPILER_PACKAGES",
"vulkan": "VULKAN_PACKAGES",
}
for distro, config in distributions.items():
lines.append(f" {distro})")
for field, variable in names.items():
values = _package_list(config.get(field), f"{distro}.{field}")
if values:
lines.append(_array_assignment(variable, values))
python_packages = config.get("python_packages")
if python_packages:
for role in sorted(SUSE_PYTHON_ROLES):
variable = f"PYTHON_{role.upper()}_SUFFIX"
lines.append(f" {variable}={shlex.quote(python_packages[role])}")
text_input = config["text_input"]
lines.append(_array_assignment("XDOTOOL_PACKAGES", text_input["xdotool"]))
lines.append(_array_assignment("WTYPE_PACKAGES", text_input["wtype"]))
lines.append(_array_assignment("YDOTOOL_PACKAGES", text_input["ydotool"]))
lines.extend([" ;;", ""])
lines.extend(
[
" *)",
' echo "Unknown distro package map key: $package_map_key" >&2',
" return 1",
" ;;",
" esac",
"}",
"",
]
)
return "\n".join(lines)
def main() -> int:
"""Generate the map, or check that the committed output is current."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true", help="fail if the output is stale")
args = parser.parse_args()
try:
generated = render(load_map())
except (OSError, ValueError, yaml.YAMLError) as error:
print(f"error: {error}", file=sys.stderr)
return 1
if args.check:
current = TARGET.read_text(encoding="utf-8") if TARGET.exists() else ""
if current != generated:
print(
f"{TARGET.relative_to(ROOT)} is stale; run `just distro-packages`",
file=sys.stderr,
)
return 1
return 0
TARGET.write_text(generated, encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+13 -7
View File
@@ -138,12 +138,12 @@ class TestCrossDistroCompatibility:
assert "typelib-1_0-Notify-0_7" in install_sh_content
assert "libnotify4" in install_sh_content
assert "suse_install_appindicator_runtime" in install_sh_content
assert "shaderc glslang-devel glslang" in install_sh_content
zypper_line = next(
line for line in install_sh_content.splitlines() if "local ZYPPER_PACKAGES=" in line
line
for line in install_sh_content.splitlines()
if "SYSTEM_PACKAGES=(gtk3 ibus " in line
)
zypper_packages = zypper_line.split('"')[1].split()
zypper_packages = zypper_line.removeprefix("SYSTEM_PACKAGES=(").rstrip(")").split()
# ibus, not the typelib alone: ibus_engine.py spawns `ibus-daemon -x -d -r`
# and shells out to `ibus engine`, neither of which libibus provides.
assert "ibus" in zypper_packages
@@ -157,10 +157,14 @@ class TestCrossDistroCompatibility:
def test_rpm_arch_install_glslc_not_glslang(self, install_sh_content):
"""ggml Vulkan needs glslc (shaderc). glslangValidator is not a substitute (#604)."""
dnf_line = next(
line for line in install_sh_content.splitlines() if "local DNF_PACKAGES=" in line
line
for line in install_sh_content.splitlines()
if "SYSTEM_PACKAGES=(python3-pip python3-gobject gtk3 " in line
)
pacman_line = next(
line for line in install_sh_content.splitlines() if "local PACMAN_PACKAGES=" in line
line
for line in install_sh_content.splitlines()
if "SYSTEM_PACKAGES=(python-pip python-gobject gtk3 " in line
)
assert "glslc" in dnf_line
assert "patchelf" in dnf_line
@@ -177,7 +181,9 @@ class TestCrossDistroCompatibility:
in install_sh_content
)
emerge_line = next(
line for line in install_sh_content.splitlines() if "local EMERGE_PACKAGES=" in line
line
for line in install_sh_content.splitlines()
if "SYSTEM_PACKAGES=(dev-python/pygobject:3 " in line
)
assert "media-libs/shaderc" in emerge_line
assert "dev-util/patchelf" in emerge_line
+204
View File
@@ -0,0 +1,204 @@
"""Guards for the YAML-owned installer package inventory."""
import importlib.util
import os
import shlex
import subprocess
import sys
from pathlib import Path
from typing import Any
import pytest
import yaml
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "scripts" / "distro-package-map.yaml"
TARGET = ROOT / "install.d" / "package_map.sh"
GENERATOR = ROOT / "scripts" / "generate_distro_package_map.py"
def _load_generator() -> Any:
spec = importlib.util.spec_from_file_location("generate_distro_package_map", GENERATOR)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def test_generated_package_map_matches_yaml() -> None:
"""A YAML edit cannot leave the installer using yesterday's packages."""
generator = _load_generator()
assert TARGET.read_text(encoding="utf-8") == generator.render(generator.load_map())
def test_generated_package_map_is_valid_bash() -> None:
"""The committed artifact must remain safe for install.sh to source."""
result = subprocess.run(["bash", "-n", str(TARGET)], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
def test_supported_debian_maps_start_at_debian_12() -> None:
"""Do not retain an EOL Debian 11 package branch in the source of truth."""
distributions = yaml.safe_load(SOURCE.read_text(encoding="utf-8"))["distributions"]
debian_maps = {name for name in distributions if name.startswith("debian_")}
assert debian_maps == {"debian_12", "debian_13_plus"}
def _resolve_debian_map(
distro_id: str, version: str, *, modern_gi: bool, refresh_fails: bool = False
) -> subprocess.CompletedProcess:
script = f"""
set -eu
source {shlex.quote(str(TARGET))}
source {shlex.quote(str(ROOT / 'install.d' / 'system_dependencies.sh'))}
print_error() {{ echo "$*"; }}
EXIT_NETWORK=3
EXIT_MISSING_DEPS=2
CACHE_READY=no
sudo() {{
[ "$*" = "apt update" ] || exit 99
echo "refreshing indexes"
[ "$REFRESH_FAILS" = no ] || return 1
CACHE_READY=yes
}}
apt-cache() {{
[ "$*" = "show $DEBIAN_13_PLUS_PROBE_PACKAGE" ] || exit 99
[ "$CACHE_READY" = yes ] && [ "$HAS_MODERN_GI" = yes ]
}}
DISTRO_ID="$1"
DISTRO_VERSION="$2"
resolve_debian_package_map_key
"""
return subprocess.run(
["bash", "-c", script, "bash", distro_id, version],
capture_output=True,
text=True,
env={
**os.environ,
"HAS_MODERN_GI": "yes" if modern_gi else "no",
"REFRESH_FAILS": "yes" if refresh_fails else "no",
},
)
def test_native_debian_version_selects_supported_map() -> None:
"""Debian itself uses VERSION_ID and rejects its EOL releases."""
assert _resolve_debian_map("debian", "11", modern_gi=False).returncode != 0
assert _resolve_debian_map("debian", "12", modern_gi=True).stdout.strip() == "debian_12"
assert _resolve_debian_map("debian", "13", modern_gi=False).stdout.strip() == "debian_13_plus"
def test_debian_derivative_ignores_unrelated_product_version() -> None:
"""A derivative's VERSION_ID must not be interpreted as a Debian release."""
legacy = _resolve_debian_map("mx", "11", modern_gi=False)
modern = _resolve_debian_map("kali", "2026.3", modern_gi=True)
assert legacy.returncode == 0
assert legacy.stdout.strip() == "debian_12"
assert modern.stdout.strip() == "debian_13_plus"
assert "refreshing indexes" in modern.stderr
def test_debian_probe_does_not_fall_back_on_network_failure() -> None:
result = _resolve_debian_map("derivative", "7", modern_gi=True, refresh_fails=True)
assert result.returncode == 3
assert "debian_12" not in result.stdout
@pytest.mark.parametrize("field", ["vulkan", "shader_compiler"])
@pytest.mark.parametrize("value", [None, []])
def test_required_suse_build_inventory(tmp_path: Path, field: str, value: Any) -> None:
generator = _load_generator()
document = yaml.safe_load(SOURCE.read_text(encoding="utf-8"))
if value is None:
del document["distributions"]["suse"][field]
else:
document["distributions"]["suse"][field] = value
generator.SOURCE = tmp_path / "invalid.yaml"
generator.SOURCE.write_text(yaml.safe_dump(document), encoding="utf-8")
with pytest.raises(ValueError, match=f"suse.{field}"):
generator.load_map()
def test_text_tool_cannot_silently_drop_second_package(tmp_path: Path) -> None:
generator = _load_generator()
document = yaml.safe_load(SOURCE.read_text(encoding="utf-8"))
document["distributions"]["ubuntu"]["text_input"]["wtype"].append("extra-package")
generator.SOURCE = tmp_path / "invalid.yaml"
generator.SOURCE.write_text(yaml.safe_dump(document), encoding="utf-8")
with pytest.raises(ValueError, match="exactly one package"):
generator.load_map()
def test_suse_suffix_spelling_is_owned_by_yaml(tmp_path: Path) -> None:
generator = _load_generator()
document = yaml.safe_load(SOURCE.read_text(encoding="utf-8"))
document["distributions"]["suse"]["python_packages"]["pip"] = "renamed-pip"
generator.SOURCE = tmp_path / "map.yaml"
generator.SOURCE.write_text(yaml.safe_dump(document), encoding="utf-8")
rendered = generator.render(generator.load_map())
assert "PYTHON_PIP_SUFFIX=renamed-pip" in rendered
def test_suse_python_roles_are_named_not_positional(tmp_path: Path) -> None:
generator = _load_generator()
document = yaml.safe_load(SOURCE.read_text(encoding="utf-8"))
del document["distributions"]["suse"]["python_packages"]["pip"]
document["distributions"]["suse"]["python_packages"]["unexpected"] = "pip"
generator.SOURCE = tmp_path / "invalid.yaml"
generator.SOURCE.write_text(yaml.safe_dump(document), encoding="utf-8")
with pytest.raises(ValueError, match="python_packages must define"):
generator.load_map()
@pytest.mark.parametrize(
"old,new",
[
("schema_version: 1", "schema_version: 99\nschema_version: 1"),
("wtype: [wtype]", "wtype: [discarded]\n wtype: [wtype]"),
],
)
def test_duplicate_keys_are_rejected(tmp_path: Path, old: str, new: str) -> None:
generator = _load_generator()
generator.SOURCE = tmp_path / "invalid.yaml"
generator.SOURCE.write_text(SOURCE.read_text().replace(old, new, 1), encoding="utf-8")
with pytest.raises(ValueError, match="duplicate YAML key"):
generator.load_map()
def test_suse_appindicator_alternatives_are_required(tmp_path: Path) -> None:
"""Generation fails before an empty openSUSE fallback loop reaches users."""
generator = _load_generator()
document = yaml.safe_load(SOURCE.read_text(encoding="utf-8"))
document["distributions"]["suse"]["appindicator"] = []
invalid_source = tmp_path / "invalid.yaml"
invalid_source.write_text(yaml.safe_dump(document), encoding="utf-8")
generator.SOURCE = invalid_source
with pytest.raises(ValueError, match="suse.appindicator"):
generator.load_map()
def test_installer_uses_generated_inventory_instead_of_package_lists() -> None:
"""Package data belongs in YAML; handwritten shell owns only selection policy."""
installer = (ROOT / "install.d" / "system_dependencies.sh").read_text(encoding="utf-8")
assert 'load_distro_package_map "$PACKAGE_MAP_KEY"' in installer
for old_variable in (
"APT_PACKAGES_UBUNTU",
"APT_PACKAGES_DEBIAN_BASE",
"DNF_PACKAGES",
"PACMAN_PACKAGES",
"ZYPPER_PACKAGES",
"EMERGE_PACKAGES",
"APK_PACKAGES",
"XBPS_PACKAGES",
"EOPKG_PACKAGES",
):
assert f"local {old_variable}=" not in installer
def test_unsupported_distros_do_not_read_uninitialized_package_arrays() -> None:
"""The existing non-interactive unsupported-distro path may continue safely."""
installer = (ROOT / "install.d" / "system_dependencies.sh").read_text(encoding="utf-8")
guard = installer.index('[[ -z "${XDOTOOL_PACKAGES+x}" ]]')
first_read = installer.index("${XDOTOOL_PACKAGES[0]}")
assert guard < first_read
+20 -30
View File
@@ -3,8 +3,13 @@
import unittest
from pathlib import Path
import yaml
INSTALLER = Path(__file__).resolve().parents[1] / "install.sh"
INSTALLER_MODULES = Path(__file__).resolve().parents[1] / "install.d"
PACKAGE_MAP = yaml.safe_load(
(INSTALLER.parent / "scripts" / "distro-package-map.yaml").read_text(encoding="utf-8")
)["distributions"]
def _installer_source() -> str:
@@ -83,40 +88,25 @@ class InstallerCudaDiagnosticsTests(unittest.TestCase):
def test_installer_includes_xsel_for_wayland_clipboard_fallback(self) -> None:
"""Fresh installs should include xsel for ydotool's layout-safe paste path."""
source = _installer_source()
for distro, config in PACKAGE_MAP.items():
for tool in ("xclip", "xsel", "wl-clipboard"):
self.assertTrue(
any(
package == tool or package.endswith(f"/{tool}")
for package in config["system"]
),
f"{distro} does not install {tool}",
)
required_lines = [
"local APT_PACKAGES_UBUNTU=",
"local APT_PACKAGES_DEBIAN_BASE=",
"local DNF_PACKAGES=",
"local PACMAN_PACKAGES=",
"local ZYPPER_PACKAGES=",
"local EMERGE_PACKAGES=",
"local APK_PACKAGES=",
"local XBPS_PACKAGES=",
"local EOPKG_PACKAGES=",
]
for name in required_lines:
line = next(line for line in source.splitlines() if name in line)
self.assertIn("xclip", line)
self.assertIn("xsel", line)
self.assertIn("wl-clipboard", line)
def test_ubuntu_22_package_list_omits_util_linux_extra(self) -> None:
def test_optional_apt_package_is_not_in_base_lists(self) -> None:
"""util-linux-extra is probed via apt-cache, not hardcoded in base package lists."""
source = _installer_source()
ubuntu_line = next(
line for line in source.splitlines() if "local APT_PACKAGES_UBUNTU=" in line
)
debian_13_line = next(
line for line in source.splitlines() if "local APT_PACKAGES_DEBIAN_13_PLUS=" in line
)
self.assertNotIn("util-linux-extra", ubuntu_line)
self.assertNotIn("util-linux-extra", debian_13_line)
self.assertIn("apt-cache show util-linux-extra", source)
self.assertIn('APT_PACKAGES="$APT_PACKAGES util-linux-extra"', source)
for distro in ("ubuntu", "debian_12", "debian_13_plus"):
self.assertNotIn("util-linux-extra", PACKAGE_MAP[distro]["system"])
self.assertIn("util-linux-extra", PACKAGE_MAP[distro]["optional_system"])
self.assertIn('apt-cache show "$OPTIONAL_PACKAGE"', source)
self.assertIn('SYSTEM_PACKAGES+=("$OPTIONAL_PACKAGE")', source)
if __name__ == "__main__":
+2
View File
@@ -12,6 +12,7 @@ EXPECTED_MODULES = {
"desktop.sh",
"interactive.sh",
"models.sh",
"package_map.sh",
"system_dependencies.sh",
}
@@ -22,6 +23,7 @@ def test_installer_loads_every_module_from_the_resolved_tree() -> None:
assert "source_installer_module()" in source
for module in EXPECTED_MODULES:
assert module in source
assert source.index("package_map.sh") < source.index("system_dependencies.sh")
remote_handoff = source.index("handoff_to_tagged_installer")
module_loader = source.index("source_installer_module()")
+8 -7
View File
@@ -15,6 +15,7 @@ import zipfile
from pathlib import Path
import pytest
import yaml
from packaging.requirements import Requirement
ROOT = Path(__file__).resolve().parents[1]
@@ -340,10 +341,9 @@ def test_source_built_runtime_packages_have_build_deps_on_split_devel_distros()
"""The pinned runtime builds these from source; distro lists that split
-devel packages must carry their build deps."""
runtime = (ROOT / "requirements/runtime.txt").read_text(encoding="utf-8")
installer = installer_source()
def package_line(prefix: str) -> str:
return next(line for line in installer.splitlines() if f"local {prefix}=" in line)
package_map = yaml.safe_load(
(ROOT / "scripts" / "distro-package-map.yaml").read_text(encoding="utf-8")
)["distributions"]
build_deps = {
"pycairo": ("cairo-devel", "libcairo2-dev"),
@@ -353,6 +353,7 @@ def test_source_built_runtime_packages_have_build_deps_on_split_devel_distros()
for name, (dnf_pkg, apt_pkg) in build_deps.items():
if not re.search(rf"^{name}==", runtime, re.MULTILINE):
continue # wheels-only or dropped: nothing to build
assert dnf_pkg in package_line("DNF_PACKAGES"), name
assert apt_pkg in package_line("APT_PACKAGES_UBUNTU"), name
assert apt_pkg in package_line("APT_PACKAGES_DEBIAN_BASE"), name
assert dnf_pkg in package_map["fedora"]["system"], name
assert apt_pkg in package_map["ubuntu"]["system"], name
assert apt_pkg in package_map["debian_12"]["system"], name
assert apt_pkg in package_map["debian_13_plus"]["system"], name
Generated
+2
View File
@@ -2958,6 +2958,7 @@ dev = [
{ name = "pytest-cov" },
{ name = "pytest-mock" },
{ name = "pytest-timeout" },
{ name = "pyyaml" },
]
docs = [
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
@@ -3016,6 +3017,7 @@ requires-dist = [
{ name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.10.0" },
{ name = "pytest-timeout", marker = "extra == 'dev'", specifier = ">=2.1.0" },
{ name = "pywhispercpp", specifier = ">=1.5.0,<2" },
{ name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6.0,<7" },
{ name = "requests", specifier = ">=2.28.0" },
{ name = "sherpa-onnx", marker = "extra == 'parakeet'", specifier = ">=1.12.0,<2" },
{ name = "sphinx", marker = "extra == 'docs'", specifier = ">=6.0.0" },