195 lines
8.3 KiB
Bash
Executable File
195 lines
8.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Run install.sh unattended in a distro container and check what it installed.
|
|
#
|
|
# Usage (a read-only mount is enough; the tree is taken via git archive):
|
|
# docker run --rm -v "$PWD:/repo:ro" debian:12 bash /repo/scripts/install-test.sh
|
|
# or: just install-gate debian:12
|
|
#
|
|
# Flags:
|
|
# --auto the only non-interactive mode; without it the script blocks
|
|
# on prompts that CI has no TTY to answer.
|
|
# --skip-models a model is 40-75 MB per container per run, and the download
|
|
# path is verified by checksum elsewhere.
|
|
# Not --skip-system-deps: the per-distro package installation is the point.
|
|
#
|
|
# install.sh refuses to run as root and calls sudo directly, so the container
|
|
# needs an unprivileged user with passwordless sudo. Running it as root would
|
|
# exercise a path no user takes.
|
|
#
|
|
# Local mode only: install.sh chooses it by finding a pyproject.toml naming
|
|
# vocalinux in the working directory, so extracting the tree and running from it
|
|
# tests this commit. Remote mode (curl | bash, which clones the latest tag) is
|
|
# not covered here.
|
|
#
|
|
# Failure mode to watch: the install pip-installs pywhispercpp against whatever
|
|
# CPython the distro ships. Wheels exist for manylinux_2_28, but when a rolling
|
|
# distro moves to a new interpreter before upstream publishes wheels for it, pip
|
|
# falls back to the sdist and this goes slow or red for an unrelated reason.
|
|
set -euo pipefail
|
|
|
|
REPO="${REPO:-/repo}"
|
|
INSTALL_USER="${INSTALL_USER:-installer}"
|
|
INSTALL_HOME="/home/$INSTALL_USER"
|
|
# Local mode puts the venv inside the tree, which is why the checkout is copied
|
|
# rather than written to: a bind-mounted host tree would come back owned by a
|
|
# container uid with a venv in it.
|
|
TREE="$INSTALL_HOME/vocalinux"
|
|
|
|
fail() {
|
|
echo "FAIL: $*" >&2
|
|
dump_install_log
|
|
exit 1
|
|
}
|
|
|
|
# install.sh writes a full transcript under ~/.local/state/vocalinux/. It is the
|
|
# only place the per-distro package manager output survives.
|
|
dump_install_log() {
|
|
local log
|
|
log="$(ls -1t "$INSTALL_HOME"/.local/state/vocalinux/install-*.log 2>/dev/null | head -n1 || true)"
|
|
if [ -n "$log" ]; then
|
|
echo "== last 200 lines of $log ==" >&2
|
|
tail -n 200 "$log" >&2
|
|
else
|
|
echo "== no install log was written ==" >&2
|
|
fi
|
|
}
|
|
|
|
# A mirror going slow mid-transaction leaves a half-populated cache, so
|
|
# re-running the same transaction resumes rather than restarts.
|
|
retry() {
|
|
local attempt
|
|
for attempt in 1 2 3; do
|
|
"$@" && return 0
|
|
echo " '$1' failed (attempt $attempt); retrying" >&2
|
|
sleep 5
|
|
done
|
|
return 1
|
|
}
|
|
|
|
[ -f "$REPO/install.sh" ] || fail "$REPO/install.sh not found; mount the checkout at \$REPO"
|
|
|
|
. /etc/os-release
|
|
echo "== ${NAME:-unknown} ${VERSION_ID:-} =="
|
|
|
|
echo "== Bootstrap: sudo, git and an unprivileged user =="
|
|
# The minimum for install.sh to start, and nothing it should install itself.
|
|
# git is for the archive below; local mode never calls it.
|
|
case "$ID" in
|
|
ubuntu | debian)
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
retry apt-get update -qq
|
|
retry apt-get install -y -qq sudo git ca-certificates >/dev/null
|
|
;;
|
|
fedora | rhel | centos)
|
|
retry dnf install -y -q sudo git shadow-utils >/dev/null
|
|
;;
|
|
arch)
|
|
# -Syu, never -Sy: archlinux:latest lags the mirrors, and a partial upgrade
|
|
# would be this script's breakage rather than the installer's.
|
|
retry pacman -Syu --noconfirm --needed sudo git >/dev/null
|
|
;;
|
|
opensuse-tumbleweed | opensuse-leap | sles)
|
|
retry zypper --non-interactive --gpg-auto-import-keys refresh >/dev/null
|
|
retry zypper --non-interactive install -y sudo git >/dev/null
|
|
;;
|
|
*)
|
|
fail "no bootstrap arm for ID='$ID'; add one when adding the container"
|
|
;;
|
|
esac
|
|
|
|
id -u "$INSTALL_USER" >/dev/null 2>&1 || useradd -m -s /bin/bash "$INSTALL_USER"
|
|
echo "$INSTALL_USER ALL=(ALL) NOPASSWD: ALL" >"/etc/sudoers.d/$INSTALL_USER"
|
|
chmod 0440 "/etc/sudoers.d/$INSTALL_USER"
|
|
|
|
echo "== Extract this commit into $TREE =="
|
|
# git archive, not cp: it takes exactly the tracked files a user gets from a
|
|
# clone, so a build artefact or stray venv in the working tree cannot make this
|
|
# pass. safe.directory because the mount is owned by another uid.
|
|
git config --global --add safe.directory '*'
|
|
git -C "$REPO" rev-parse --verify HEAD >/dev/null \
|
|
|| fail "$REPO is not a git checkout; git archive needs it"
|
|
install -d -o "$INSTALL_USER" -g "$INSTALL_USER" "$TREE"
|
|
git -C "$REPO" archive HEAD | tar -x -C "$TREE"
|
|
chown -R "$INSTALL_USER:$INSTALL_USER" "$TREE"
|
|
|
|
# --skip-models below means install.sh never reads this manifest, so nothing in
|
|
# this run would notice it leaving the tree. #736 was the release-tag half of
|
|
# that shape: an installer verifying models against a manifest the tree it had
|
|
# just cloned did not carry. This is the half a local-mode gate can see.
|
|
[ -f "$TREE/src/vocalinux/utils/model_checksums.txt" ] \
|
|
|| fail "the tree ships no src/vocalinux/utils/model_checksums.txt; install.sh pins model downloads against it"
|
|
|
|
echo "== Run install.sh --auto --skip-models as $INSTALL_USER =="
|
|
# su -, so HOME and PATH are the user's own: install.sh writes wrappers into
|
|
# $HOME/.local/bin and reads $HOME throughout, and a run with root's HOME would
|
|
# install into the wrong place and still succeed.
|
|
START=$(date +%s)
|
|
if ! su - "$INSTALL_USER" -c "cd '$TREE' && bash install.sh --auto --skip-models"; then
|
|
fail "install.sh exited non-zero"
|
|
fi
|
|
echo " install took $(( $(date +%s) - START ))s"
|
|
|
|
echo "== Smoke: is there an installation, and does it import =="
|
|
VENV="$TREE/venv"
|
|
[ -x "$VENV/bin/python" ] || fail "no venv interpreter at $VENV/bin/python"
|
|
[ -x "$INSTALL_HOME/.local/bin/vocalinux" ] || fail "no wrapper at ~/.local/bin/vocalinux"
|
|
[ -f "$INSTALL_HOME/.local/share/applications/vocalinux.desktop" ] \
|
|
|| fail "no desktop entry; install_desktop_entry did not run"
|
|
|
|
# The injection backends are reached through subprocess, not GI, so no typelib
|
|
# import can see one of these missing. These are the binaries every distro's
|
|
# package list promises.
|
|
for tool in xclip xsel wl-copy; do
|
|
command -v "$tool" >/dev/null 2>&1 || fail "$tool is not on PATH; install.sh lists it for every distro"
|
|
done
|
|
|
|
# ibus-daemon and the ibus CLI are what ibus_engine.py actually spawns, and only
|
|
# the arch and suse lists carry the package that provides them. Fedora's
|
|
# ibus-devel and Ubuntu's gir1.2-ibus-1.0 pull the library and the typelib
|
|
# alone, so this records the split rather than hiding it: on the two distros
|
|
# that do promise the runtime, a swap to a typelib-only package fails here
|
|
# instead of on a user's first dictation.
|
|
case "$ID" in
|
|
arch | opensuse-tumbleweed | opensuse-leap | sles)
|
|
command -v ibus-daemon >/dev/null 2>&1 \
|
|
|| fail "no ibus-daemon: the package list ships the IBus typelib without the runtime ibus_engine.py spawns"
|
|
;;
|
|
esac
|
|
|
|
# Through the wrapper rather than the venv directly: the wrapper is what the
|
|
# desktop entry and the user's PATH invoke, and it carries the GI_TYPELIB_PATH
|
|
# and pywhispercpp library-path resolution the bare console script does not.
|
|
su - "$INSTALL_USER" -c "'$INSTALL_HOME/.local/bin/vocalinux' --version" \
|
|
|| fail "the installed wrapper cannot report a version"
|
|
|
|
su - "$INSTALL_USER" -c "'$VENV/bin/python' - " <<'PY' || fail "the installation does not import"
|
|
import vocalinux.main
|
|
import vocalinux.ui.tray_indicator
|
|
import vocalinux.speech_recognition.recognition_manager
|
|
import vocalinux.text_injection.text_injector
|
|
from vocalinux.ui.keyboard_backends import EVDEV_AVAILABLE, PYNPUT_AVAILABLE
|
|
import gi
|
|
|
|
# keyboard_backends imports each backend under `except ImportError`, so the
|
|
# import above stays clean when evdev and pynput both failed to build. That is
|
|
# the openSUSE failure this gate was extended for, so assert the flags rather
|
|
# than the import.
|
|
assert EVDEV_AVAILABLE or PYNPUT_AVAILABLE, (
|
|
"no keyboard backend is importable: evdev and pynput both failed to install"
|
|
)
|
|
|
|
# The typelibs the installer is responsible for putting on the system. Both are
|
|
# loaded lazily at runtime, so importing the modules above proves nothing about
|
|
# them: a missing one would surface on a user's first dictation instead.
|
|
gi.require_version("Gtk", "3.0")
|
|
gi.require_version("IBus", "1.0")
|
|
from gi.repository import Gtk, IBus # noqa: F401
|
|
|
|
print(
|
|
" main, tray, recognition, injection, Gtk and IBus import;"
|
|
f" keyboard backends: evdev={EVDEV_AVAILABLE} pynput={PYNPUT_AVAILABLE}"
|
|
)
|
|
PY
|
|
|
|
echo "PASS: install.sh installs on ${NAME:-this distro} (this commit, unattended)"
|