feat(void): add xbps template for the voculinux app
Ticket xavierk/voculinux#27, built on #18 (rename) plus the #25/#26 dependency templates. The template builds the app from the canonical checksum-verified release sdist, depends on Void's interpreter, GTK, desktop bindings and packaged Python libraries, and installs the renamed desktop entry, icons and a packaged-install marker. No speech models, no vendored wheels, no pip at build or install time. Packaged installs keep background update checks as a signal, but every update affordance presents the package-manager path (src/vocalinux/utils/packaging.py). Also corrects the Void distro inventory (Ayatana provider name; ydotool marked absent) and regens the installer map.
This commit is contained in:
@@ -193,10 +193,14 @@ sudo apk add py3-gobject3 py3-pip gtk+3.0 py3-cairo portaudio-dev \
|
|||||||
|
|
||||||
### Void Linux
|
### Void Linux
|
||||||
```bash
|
```bash
|
||||||
sudo xbps-install -Sy python3-pip python3-gobject gtk+3 libappindicator \
|
sudo xbps-install -Sy python3-pip python3-gobject gtk+3 libayatana-appindicator \
|
||||||
gobject-introspection portaudio-devel python3-devel pkg-config
|
gobject-introspection portaudio-devel python3-devel pkg-config
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note: `ydotool` is not packaged for Void, so the ydotool injection
|
||||||
|
fallback rung is unavailable there; the app uses IBus, the RemoteDesktop
|
||||||
|
portal, `wtype` or `xdotool` instead.
|
||||||
|
|
||||||
### Solus
|
### Solus
|
||||||
```bash
|
```bash
|
||||||
sudo eopkg install python3-pip python3-gobject gtk3 libappindicator \
|
sudo eopkg install python3-pip python3-gobject gtk3 libappindicator \
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ load_distro_package_map() {
|
|||||||
;;
|
;;
|
||||||
|
|
||||||
void)
|
void)
|
||||||
SYSTEM_PACKAGES=(python3-pip python3-gobject gtk+3 libappindicator-gtk3 gobject-introspection portaudio-devel cairo-devel python3-devel pkg-config cmake base-devel wget curl unzip shaderc patchelf Vulkan-Tools xclip xsel wl-clipboard)
|
SYSTEM_PACKAGES=(python3-pip python3-gobject gtk+3 libayatana-appindicator gobject-introspection portaudio-devel cairo-devel python3-devel pkg-config cmake base-devel wget curl unzip shaderc patchelf Vulkan-Tools xclip xsel wl-clipboard)
|
||||||
XDOTOOL_PACKAGES=(xdotool)
|
XDOTOOL_PACKAGES=(xdotool)
|
||||||
WTYPE_PACKAGES=(wtype)
|
WTYPE_PACKAGES=(wtype)
|
||||||
YDOTOOL_PACKAGES=(ydotool)
|
YDOTOOL_PACKAGES=(ydotool)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ packaging/void/
|
|||||||
| Template | Ticket | Notes |
|
| Template | Ticket | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `python3-pywhispercpp` | #26 | Vulkan backend on by default; see below |
|
| `python3-pywhispercpp` | #26 | Vulkan backend on by default; see below |
|
||||||
|
| `voculinux` | #27 | The app, from the canonical release sdist; see below |
|
||||||
|
|
||||||
## python3-pywhispercpp
|
## python3-pywhispercpp
|
||||||
|
|
||||||
@@ -61,11 +62,45 @@ for the sdist; `tests/test_void_pywhispercpp_template.py` fails otherwise.
|
|||||||
verified to exist in current void-packages; the container build
|
verified to exist in current void-packages; the container build
|
||||||
re-verifies via dependency resolution.
|
re-verifies via dependency resolution.
|
||||||
|
|
||||||
|
## voculinux
|
||||||
|
|
||||||
|
Version `0.18.5`, matching `src/vocalinux/version.py` — never newest
|
||||||
|
upstream. The template builds the canonical release source distribution
|
||||||
|
(the `releases/download` sdist, the same bytes published to PyPI) with a
|
||||||
|
pinned `checksum`; `tests/test_void_app_template.py` fails otherwise.
|
||||||
|
The 0.18.5 asset still carries the pre-rename archive root, so the
|
||||||
|
template sets `wrksrc` explicitly; the first release cut after the
|
||||||
|
rename drops the override and points `distfiles` at the renamed asset.
|
||||||
|
|
||||||
|
- **Runtime deps** are Void's own interpreter, GTK and desktop bindings
|
||||||
|
(`python3`, `python3-gobject`, `python3-cairo`, `gtk+3`,
|
||||||
|
`libayatana-appindicator`, `ibus`, `gobject-introspection`), the
|
||||||
|
packaged Python libraries, and the three templates from tickets #25
|
||||||
|
and #26 (`python3-pynput`, `python3-PyAudio`, `python3-pywhispercpp`).
|
||||||
|
Every name was verified to exist in current void-packages; the
|
||||||
|
container build re-verifies via dependency resolution.
|
||||||
|
- **No payload beyond the app**: no speech models (they download on
|
||||||
|
first run), no vendored wheels, no pip at build or install time. The
|
||||||
|
container smoke fails the build if the payload carries model weights
|
||||||
|
or wheels.
|
||||||
|
- **Renamed paths**: the `voculinux.desktop` entry, the hicolor icons,
|
||||||
|
and a `usr/share/voculinux/packaged` marker the app reads to present
|
||||||
|
package-manager updates.
|
||||||
|
- **INSTALL.msg** covers what xbps cannot express: the `input` group for
|
||||||
|
the evdev hotkey backend, first-run model downloads, optional engines
|
||||||
|
as manual additions (vosk, OpenAI whisper, Parakeet, faster-whisper,
|
||||||
|
onnxruntime), the absent `ydotool` rung, and updating through xbps.
|
||||||
|
- **Update checker**: packaged installs keep background checks as a
|
||||||
|
signal, but every update affordance (tray entry, notification, About
|
||||||
|
page) presents the package-manager path
|
||||||
|
(`src/vocalinux/utils/packaging.py`).
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
packaging/void/build.sh python3-pywhispercpp
|
packaging/void/build.sh python3-pywhispercpp
|
||||||
packaging/void/build.sh python3-pywhispercpp -- -o ~vulkan pkg
|
packaging/void/build.sh python3-pywhispercpp -- -o ~vulkan pkg
|
||||||
|
packaging/void/build.sh voculinux
|
||||||
```
|
```
|
||||||
|
|
||||||
Podman only, never Docker. The base image digest and the void-packages
|
Podman only, never Docker. The base image digest and the void-packages
|
||||||
|
|||||||
@@ -31,6 +31,32 @@ echo "built: $PKGFILE"
|
|||||||
xbps-install -Sy >/dev/null
|
xbps-install -Sy >/dev/null
|
||||||
xbps-install -y --repository="$(dirname "$PKGFILE")" "$PKGNAME" >/dev/null
|
xbps-install -y --repository="$(dirname "$PKGFILE")" "$PKGNAME" >/dev/null
|
||||||
|
|
||||||
|
case "$PKGNAME" in
|
||||||
|
voculinux)
|
||||||
|
echo "== payload $PKGNAME: desktop entry, icons, packaged marker"
|
||||||
|
tar -tf "$PKGFILE" 2>/dev/null | grep -q "usr/share/applications/voculinux.desktop" || {
|
||||||
|
echo "renamed desktop entry missing from the built package" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
tar -tf "$PKGFILE" 2>/dev/null | grep -q "usr/share/icons/hicolor/scalable/apps/voculinux.svg" || {
|
||||||
|
echo "renamed icons missing from the built package" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
tar -tf "$PKGFILE" 2>/dev/null | grep -q "usr/share/voculinux/packaged" || {
|
||||||
|
echo "packaged-install marker missing from the built package" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "== payload $PKGNAME: no speech models, no vendored wheels"
|
||||||
|
if tar -tf "$PKGFILE" 2>/dev/null | grep -E "models/|\.ggml$|\.bin$|\.pt$|\.whl$"; then
|
||||||
|
echo "package payload carries models or wheels" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "no models or wheels in the shipped package"
|
||||||
|
echo "== version and import smoke $PKGNAME"
|
||||||
|
voculinux --version
|
||||||
|
python3 -c "from importlib.metadata import version; print('voculinux ' + version('voculinux'))"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
echo "== compiler flag scan of the shipped ggml library"
|
echo "== compiler flag scan of the shipped ggml library"
|
||||||
LIBMEMBER=$(tar -tf "$PKGFILE" 2>/dev/null | grep -E "libggml-cpu\.so" | head -n1)
|
LIBMEMBER=$(tar -tf "$PKGFILE" 2>/dev/null | grep -E "libggml-cpu\.so" | head -n1)
|
||||||
[ -n "$LIBMEMBER" ] || {
|
[ -n "$LIBMEMBER" ] || {
|
||||||
@@ -71,3 +97,5 @@ assert has_vulkan == want_vulkan, "vulkan backend present=%s, expected=%s" % (ha
|
|||||||
print("smoke ok")
|
print("smoke ok")
|
||||||
PYEOF
|
PYEOF
|
||||||
EXPECT_VULKAN="$EXPECT_VULKAN" python3 /smoke.py
|
EXPECT_VULKAN="$EXPECT_VULKAN" python3 /smoke.py
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
Voculinux installed system-wide from an xbps package.
|
||||||
|
|
||||||
|
Hotkeys need the input group: add your user with
|
||||||
|
sudo usermod -aG input <user>
|
||||||
|
then log out and back in, or the evdev hotkey
|
||||||
|
backend cannot open /dev/input devices.
|
||||||
|
|
||||||
|
Speech models are not in this package: they
|
||||||
|
download on first run into ~/.local/share/voculinux.
|
||||||
|
|
||||||
|
Optional engines are manual additions outside the
|
||||||
|
guaranteed set: vosk, OpenAI whisper, Parakeet
|
||||||
|
(sherpa-onnx), faster-whisper and onnxruntime (VAD)
|
||||||
|
come from your own Python environment, not xbps.
|
||||||
|
|
||||||
|
ydotool is not packaged for Void, so the ydotool
|
||||||
|
injection fallback rung is absent; the app uses
|
||||||
|
IBus, the RemoteDesktop portal, wtype or xdotool.
|
||||||
|
|
||||||
|
Update through your package manager instead of the
|
||||||
|
in-app release downloads:
|
||||||
|
sudo xbps-install -Syu voculinux
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Template file for 'voculinux'
|
||||||
|
pkgname=voculinux
|
||||||
|
version=0.18.5
|
||||||
|
revision=1
|
||||||
|
build_style=python3-module
|
||||||
|
hostmakedepends="python3-setuptools"
|
||||||
|
makedepends="python3-devel"
|
||||||
|
depends="python3 python3-gobject python3-cairo gtk+3 libayatana-appindicator ibus gobject-introspection python3-numpy python3-requests python3-pysocks python3-psutil python3-evdev python3-xlib python3-tqdm python3-platformdirs python3-PyAudio python3-pynput python3-pywhispercpp"
|
||||||
|
short_desc="Free, offline voice dictation for Linux"
|
||||||
|
maintainer="Jatin K Malik <jatinkrmalik@gmail.com>"
|
||||||
|
license="AGPL-3.0-only"
|
||||||
|
homepage="https://github.com/VocaHQ/vocalinux"
|
||||||
|
distfiles="https://github.com/VocaHQ/vocalinux/releases/download/v${version}/vocalinux-${version}.tar.gz"
|
||||||
|
checksum=1def4d8f0f14f2df2f2fee64ca29e6865e3d82e551f7d465c2a78ec6d4d72b1d
|
||||||
|
# The 0.18.5 release asset still carries the pre-rename archive root, so
|
||||||
|
# build under that directory. The first release cut after the rename ships
|
||||||
|
# the archive under the new name; that bump drops this override and points
|
||||||
|
# distfiles at the renamed asset with its published digest.
|
||||||
|
wrksrc="vocalinux-${version}"
|
||||||
|
|
||||||
|
post_install() {
|
||||||
|
# Entry points land on PATH through the module build; install the
|
||||||
|
# renamed desktop entry, the full icon set, and the marker the app
|
||||||
|
# reads to present package-manager updates (see INSTALL.msg).
|
||||||
|
vlicense LICENSE
|
||||||
|
vinstall voculinux.desktop 644 usr/share/applications
|
||||||
|
for icon in resources/icons/scalable/*.svg; do
|
||||||
|
vinstall "$icon" 644 usr/share/icons/hicolor/scalable/apps
|
||||||
|
done
|
||||||
|
vmkdir usr/share/voculinux
|
||||||
|
touch "${DESTDIR}/usr/share/voculinux/packaged"
|
||||||
|
}
|
||||||
@@ -177,10 +177,16 @@ distributions:
|
|||||||
void:
|
void:
|
||||||
# cairo-devel is needed because the pinned pycairo builds from source.
|
# cairo-devel is needed because the pinned pycairo builds from source.
|
||||||
# base-devel compiles the pywhispercpp sdist and the evdev/PyAudio sdists.
|
# base-devel compiles the pywhispercpp sdist and the evdev/PyAudio sdists.
|
||||||
system: [python3-pip, python3-gobject, gtk+3, libappindicator-gtk3, gobject-introspection, portaudio-devel, cairo-devel, python3-devel, pkg-config, cmake, base-devel, wget, curl, unzip, shaderc, patchelf, Vulkan-Tools, xclip, xsel, wl-clipboard]
|
# libayatana-appindicator is the tray provider: the libappindicator-gtk3
|
||||||
|
# name has no void-packages template (verified 2026-11-06).
|
||||||
|
system: [python3-pip, python3-gobject, gtk+3, libayatana-appindicator, gobject-introspection, portaudio-devel, cairo-devel, python3-devel, pkg-config, cmake, base-devel, wget, curl, unzip, shaderc, patchelf, Vulkan-Tools, xclip, xsel, wl-clipboard]
|
||||||
text_input:
|
text_input:
|
||||||
xdotool: [xdotool]
|
xdotool: [xdotool]
|
||||||
wtype: [wtype]
|
wtype: [wtype]
|
||||||
|
# ydotool is absent in void-packages (no srcpkgs template as of
|
||||||
|
# 2026-11-06), so the installer never attempts it on Void and the
|
||||||
|
# app's ydotool injection rung stays unavailable there. The entry
|
||||||
|
# keeps its shape so the generator contract is unchanged.
|
||||||
ydotool: [ydotool]
|
ydotool: [ydotool]
|
||||||
|
|
||||||
solus:
|
solus:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import gi
|
|||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk # noqa: E402
|
from gi.repository import Gtk # noqa: E402
|
||||||
|
|
||||||
|
from ..utils.packaging import is_packaged_install, updates_description
|
||||||
from ..utils.update_checker import (
|
from ..utils.update_checker import (
|
||||||
DEFAULT_CHECK_AUTOMATICALLY,
|
DEFAULT_CHECK_AUTOMATICALLY,
|
||||||
build_channel,
|
build_channel,
|
||||||
@@ -434,10 +435,7 @@ class AboutSectionMixin(Gtk.Dialog):
|
|||||||
build = build_channel()
|
build = build_channel()
|
||||||
updates_group = PreferencesGroup(
|
updates_group = PreferencesGroup(
|
||||||
title="Updates",
|
title="Updates",
|
||||||
description=(
|
description=updates_description(build, packaged=is_packaged_install()),
|
||||||
f"This is a {build} build, so background checks only look for "
|
|
||||||
f"{build} releases. Pick a channel below to look one up by hand."
|
|
||||||
),
|
|
||||||
keywords=("update", "version", "release", "upgrade", "appimage", "nightly", "channel"),
|
keywords=("update", "version", "release", "upgrade", "appimage", "nightly", "channel"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,12 @@ from ..speech_recognition.diarization import (
|
|||||||
transcribe_audio_file,
|
transcribe_audio_file,
|
||||||
)
|
)
|
||||||
from ..suspend_handler import SuspendHandler
|
from ..suspend_handler import SuspendHandler
|
||||||
|
from ..utils.packaging import (
|
||||||
|
is_packaged_install,
|
||||||
|
update_menu_label,
|
||||||
|
update_menu_tooltip,
|
||||||
|
update_notification_body,
|
||||||
|
)
|
||||||
from ..utils.resource_manager import ResourceManager
|
from ..utils.resource_manager import ResourceManager
|
||||||
from ..utils.update_checker import DEFAULT_CHECK_AUTOMATICALLY, ReleaseInfo
|
from ..utils.update_checker import DEFAULT_CHECK_AUTOMATICALLY, ReleaseInfo
|
||||||
from ..utils.update_monitor import UpdateMonitor
|
from ..utils.update_monitor import UpdateMonitor
|
||||||
@@ -1572,11 +1578,9 @@ class TrayIndicator:
|
|||||||
"""Reveal the tray menu entry that opens About / release notes."""
|
"""Reveal the tray menu entry that opens About / release notes."""
|
||||||
if self._update_menu_item is None:
|
if self._update_menu_item is None:
|
||||||
return
|
return
|
||||||
label = f"Update Available ({release.tag_name})…"
|
packaged = is_packaged_install()
|
||||||
self._update_menu_item.set_label(label)
|
self._update_menu_item.set_label(update_menu_label(release.tag_name, packaged=packaged))
|
||||||
self._update_menu_item.set_tooltip_text(
|
self._update_menu_item.set_tooltip_text(update_menu_tooltip(packaged=packaged))
|
||||||
"Open Settings → About for release notes and download links"
|
|
||||||
)
|
|
||||||
self._update_menu_item.show()
|
self._update_menu_item.show()
|
||||||
self._sync_indicator_menu()
|
self._sync_indicator_menu()
|
||||||
|
|
||||||
@@ -1601,8 +1605,7 @@ class TrayIndicator:
|
|||||||
"-a",
|
"-a",
|
||||||
TRAY_TITLE,
|
TRAY_TITLE,
|
||||||
f"{TRAY_TITLE} update available",
|
f"{TRAY_TITLE} update available",
|
||||||
f"{release.tag_name} is ready. Open the tray menu or "
|
update_notification_body(release.tag_name, packaged=is_packaged_install()),
|
||||||
"Settings → About for release notes.",
|
|
||||||
],
|
],
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""Facts about installs managed by a system package manager.
|
||||||
|
|
||||||
|
Ticket #27 (Void app template): the xbps template installs an empty
|
||||||
|
``usr/share/voculinux/packaged`` marker. When it exists, background
|
||||||
|
update checks stay available as a signal, but every user-visible update
|
||||||
|
affordance presents the package-manager path instead of release
|
||||||
|
downloads, so packaged users are never misdirected away from
|
||||||
|
package-manager updates.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
#: Installed by the xbps template's post_install; absent everywhere else.
|
||||||
|
PACKAGED_MARKER_PATH = Path("/usr/share/voculinux/packaged")
|
||||||
|
|
||||||
|
#: Presented wherever a packaged install would otherwise offer a download.
|
||||||
|
PACKAGED_UPDATE_HINT = (
|
||||||
|
"Installed from a system package: update with your package manager "
|
||||||
|
"(on Void: sudo xbps-install -Syu voculinux)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_packaged_install(marker: Optional[Path] = None) -> bool:
|
||||||
|
"""Return whether this process runs from a system-packaged install."""
|
||||||
|
return (PACKAGED_MARKER_PATH if marker is None else marker).is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def update_menu_label(tag_name: str, *, packaged: bool) -> str:
|
||||||
|
"""Return the tray update entry label for this install kind."""
|
||||||
|
if packaged:
|
||||||
|
return f"Update Available ({tag_name}, via package manager)…"
|
||||||
|
return f"Update Available ({tag_name})…"
|
||||||
|
|
||||||
|
|
||||||
|
def update_menu_tooltip(*, packaged: bool) -> str:
|
||||||
|
"""Return the tray update entry tooltip for this install kind."""
|
||||||
|
if packaged:
|
||||||
|
return (
|
||||||
|
"Open Settings → About for release notes; "
|
||||||
|
"update with your package manager (on Void: sudo xbps-install -Syu voculinux)"
|
||||||
|
)
|
||||||
|
return "Open Settings → About for release notes and download links"
|
||||||
|
|
||||||
|
|
||||||
|
def update_notification_body(tag_name: str, *, packaged: bool) -> str:
|
||||||
|
"""Return the update notification body for this install kind."""
|
||||||
|
if packaged:
|
||||||
|
return (
|
||||||
|
f"{tag_name} is ready. Update with your package manager "
|
||||||
|
"instead of downloading a release."
|
||||||
|
)
|
||||||
|
return f"{tag_name} is ready. Open the tray menu or Settings → About for release notes."
|
||||||
|
|
||||||
|
|
||||||
|
def updates_description(build: str, *, packaged: bool) -> str:
|
||||||
|
"""Return the About-page Updates group description for this install kind."""
|
||||||
|
description = (
|
||||||
|
f"This is a {build} build, so background checks only look for "
|
||||||
|
f"{build} releases. Pick a channel below to look one up by hand."
|
||||||
|
)
|
||||||
|
if packaged:
|
||||||
|
return f"{description} {PACKAGED_UPDATE_HINT}"
|
||||||
|
return description
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"""Unit guards for packaged-install detection and its update presentation.
|
||||||
|
|
||||||
|
Ticket #27 decides the update-checker behaviour for packaged installs:
|
||||||
|
background checks stay available as a signal, but every user-visible
|
||||||
|
update affordance presents the package-manager path instead of release
|
||||||
|
downloads. The xbps template installs the marker this module reads.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from vocalinux.utils.packaging import (
|
||||||
|
PACKAGED_MARKER_PATH,
|
||||||
|
PACKAGED_UPDATE_HINT,
|
||||||
|
is_packaged_install,
|
||||||
|
update_menu_label,
|
||||||
|
update_menu_tooltip,
|
||||||
|
update_notification_body,
|
||||||
|
updates_description,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_marker_lives_under_the_renamed_share_dir() -> None:
|
||||||
|
"""The template installs usr/share/voculinux/packaged; the code reads it."""
|
||||||
|
assert PACKAGED_MARKER_PATH == Path("/usr/share/voculinux/packaged")
|
||||||
|
|
||||||
|
|
||||||
|
def test_unpackaged_install_by_default(tmp_path: Path) -> None:
|
||||||
|
"""No marker in the test environment means a non-packaged install."""
|
||||||
|
assert not is_packaged_install(marker=tmp_path / "packaged")
|
||||||
|
|
||||||
|
|
||||||
|
def test_packaged_install_when_the_marker_exists(tmp_path: Path) -> None:
|
||||||
|
"""The xbps template drops the marker file at install time."""
|
||||||
|
marker = tmp_path / "packaged"
|
||||||
|
marker.touch()
|
||||||
|
assert is_packaged_install(marker=marker)
|
||||||
|
|
||||||
|
|
||||||
|
def test_packaged_hint_names_the_package_manager_path() -> None:
|
||||||
|
"""The presented path is xbps, not a release download."""
|
||||||
|
assert "package manager" in PACKAGED_UPDATE_HINT
|
||||||
|
assert "xbps-install -Syu voculinux" in PACKAGED_UPDATE_HINT
|
||||||
|
|
||||||
|
|
||||||
|
def test_unpackaged_menu_label_is_unchanged() -> None:
|
||||||
|
"""Source installs keep the release-download affordance they always had."""
|
||||||
|
assert update_menu_label("v1.2.3", packaged=False) == "Update Available (v1.2.3)…"
|
||||||
|
|
||||||
|
|
||||||
|
def test_packaged_menu_label_presents_the_package_manager() -> None:
|
||||||
|
"""Packaged installs must not misdirect users to release downloads."""
|
||||||
|
label = update_menu_label("v1.2.3", packaged=True)
|
||||||
|
assert "v1.2.3" in label
|
||||||
|
assert "package manager" in label
|
||||||
|
|
||||||
|
|
||||||
|
def test_unpackaged_menu_tooltip_is_unchanged() -> None:
|
||||||
|
"""Source installs keep pointing at release notes and download links."""
|
||||||
|
tooltip = update_menu_tooltip(packaged=False)
|
||||||
|
assert "release notes" in tooltip and "download links" in tooltip
|
||||||
|
|
||||||
|
|
||||||
|
def test_packaged_menu_tooltip_names_xbps() -> None:
|
||||||
|
"""The packaged tooltip tells the user where the update comes from."""
|
||||||
|
assert "xbps" in update_menu_tooltip(packaged=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_packaged_notification_body_presents_the_package_manager() -> None:
|
||||||
|
"""The desktop notification must not offer a download path either."""
|
||||||
|
body = update_notification_body("v1.2.3", packaged=True)
|
||||||
|
assert "v1.2.3" in body
|
||||||
|
assert "package manager" in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_unpackaged_notification_body_keeps_release_notes() -> None:
|
||||||
|
"""Source installs keep the existing notification wording."""
|
||||||
|
body = update_notification_body("v1.2.3", packaged=False)
|
||||||
|
assert "v1.2.3" in body
|
||||||
|
assert "release notes" in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_unpackaged_updates_description_is_unchanged() -> None:
|
||||||
|
"""The About page keeps its channel wording for source installs."""
|
||||||
|
description = updates_description("stable", packaged=False)
|
||||||
|
assert "stable" in description
|
||||||
|
assert "package manager" not in description
|
||||||
|
|
||||||
|
|
||||||
|
def test_packaged_updates_description_presents_the_package_manager() -> None:
|
||||||
|
"""The About page tells packaged users to update through xbps."""
|
||||||
|
description = updates_description("stable", packaged=True)
|
||||||
|
assert "stable" in description
|
||||||
|
assert "package manager" in description
|
||||||
@@ -110,6 +110,13 @@ def _allowed(path: str, line: str) -> bool:
|
|||||||
if 'PKG_NAME="vocalinux"' in line:
|
if 'PKG_NAME="vocalinux"' in line:
|
||||||
# Installer probes the retained import package, not the distribution.
|
# Installer probes the retained import package, not the distribution.
|
||||||
return True
|
return True
|
||||||
|
if path == "packaging/void/srcpkgs/voculinux/template" and "vocalinux-${version}" in line:
|
||||||
|
# Pre-rename release asset root (ticket #27); dropped at the first
|
||||||
|
# post-rename release bump.
|
||||||
|
return True
|
||||||
|
if path == "tests/test_void_app_template.py" and '"vocalinux"' in line:
|
||||||
|
# The app-template policy test asserts the old name is gone.
|
||||||
|
return True
|
||||||
if '== "vocalinux"' in line or '.startswith("vocalinux' in line:
|
if '== "vocalinux"' in line or '.startswith("vocalinux' in line:
|
||||||
# Retained module identity comparisons in test isolation helpers.
|
# Retained module identity comparisons in test isolation helpers.
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -0,0 +1,379 @@
|
|||||||
|
"""Offline policy guards for the Void application template (ticket #27).
|
||||||
|
|
||||||
|
Spec #16 ships Voculinux itself as a Void package: an in-repo xbps
|
||||||
|
template in void-packages layout under ``packaging/void`` builds the app
|
||||||
|
from the canonical, checksum-verified release source distribution,
|
||||||
|
depends on Void's own interpreter, GTK, desktop bindings and packaged
|
||||||
|
Python libraries plus the dependency templates from tickets #25 and #26,
|
||||||
|
and installs the renamed desktop entry and icons. Speech models are not
|
||||||
|
package payload, nothing is vendored, and no pip runs at build or
|
||||||
|
install time.
|
||||||
|
|
||||||
|
Independent sources of truth: ``src/vocalinux/version.py`` owns the
|
||||||
|
version, ``pyproject.toml`` owns the dependency ranges, ``MANIFEST.in``
|
||||||
|
plus the renamed ``voculinux.desktop`` own the install paths, and the
|
||||||
|
GitHub release publish path (``releases/download``) plus ``xbps-src``
|
||||||
|
conventions own the fetch shape. The template must agree with all of
|
||||||
|
them; the container smoke gate (ticket #28) builds on top.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
VOID = REPO_ROOT / "packaging" / "void"
|
||||||
|
SRC = VOID / "srcpkgs"
|
||||||
|
TEMPLATE = SRC / "voculinux" / "template"
|
||||||
|
INSTALL_MSG = SRC / "voculinux" / "INSTALL.msg"
|
||||||
|
PINS = VOID / "tool_checksums.txt"
|
||||||
|
BUILD_SH = VOID / "build.sh"
|
||||||
|
CONTAINER_MAIN = VOID / "container-main.sh"
|
||||||
|
VERSION_PY = REPO_ROOT / "src" / "vocalinux" / "version.py"
|
||||||
|
PYPROJECT = REPO_ROOT / "pyproject.toml"
|
||||||
|
DISTRO_MAP = REPO_ROOT / "scripts" / "distro-package-map.yaml"
|
||||||
|
DISTRO_DOC = REPO_ROOT / "docs" / "DISTRO_COMPATIBILITY.md"
|
||||||
|
|
||||||
|
#: pip name (lowercase, dashes) -> the Void binary package providing it.
|
||||||
|
#: pywhispercpp/pynput/PyAudio come from the ticket #25/#26 templates in
|
||||||
|
#: this same tree; the rest are Void's own packages.
|
||||||
|
PIP_TO_VOID = {
|
||||||
|
"pywhispercpp": "python3-pywhispercpp",
|
||||||
|
"pynput": "python3-pynput",
|
||||||
|
"evdev": "python3-evdev",
|
||||||
|
"requests": "python3-requests",
|
||||||
|
"pysocks": "python3-pysocks",
|
||||||
|
"numpy": "python3-numpy",
|
||||||
|
"pyaudio": "python3-PyAudio",
|
||||||
|
"pygobject": "python3-gobject",
|
||||||
|
"psutil": "python3-psutil",
|
||||||
|
}
|
||||||
|
|
||||||
|
#: Void packages beyond the direct pip mapping: pywhispercpp's own
|
||||||
|
#: install_requires (tqdm, platformdirs), pynput's X11 backend (xlib),
|
||||||
|
#: the directly imported cairo binding, and the desktop substrate.
|
||||||
|
EXTRA_VOID_DEPS = (
|
||||||
|
"python3-tqdm",
|
||||||
|
"python3-platformdirs",
|
||||||
|
"python3-xlib",
|
||||||
|
"python3-cairo",
|
||||||
|
"python3",
|
||||||
|
"gtk+3",
|
||||||
|
"libayatana-appindicator",
|
||||||
|
"ibus",
|
||||||
|
"gobject-introspection",
|
||||||
|
)
|
||||||
|
|
||||||
|
#: Every package name the app template may reference, verified to exist in
|
||||||
|
#: void-packages master on 2026-11-06 via the srcpkgs/<name>/template
|
||||||
|
#: paths (python3-devel is a subpackage of the python3 template, as
|
||||||
|
#: recorded for ticket #25). Re-verify with `xbps-query -Rs <name>` on a
|
||||||
|
#: Void system before extending this set.
|
||||||
|
VERIFIED_VOID_PACKAGES = frozenset(
|
||||||
|
{
|
||||||
|
"python3",
|
||||||
|
"python3-devel",
|
||||||
|
"python3-setuptools",
|
||||||
|
"python3-gobject",
|
||||||
|
"python3-cairo",
|
||||||
|
"gtk+3",
|
||||||
|
"libayatana-appindicator",
|
||||||
|
"ibus",
|
||||||
|
"gobject-introspection",
|
||||||
|
"python3-numpy",
|
||||||
|
"python3-requests",
|
||||||
|
"python3-pysocks",
|
||||||
|
"python3-psutil",
|
||||||
|
"python3-evdev",
|
||||||
|
"python3-xlib",
|
||||||
|
"python3-tqdm",
|
||||||
|
"python3-platformdirs",
|
||||||
|
"python3-PyAudio",
|
||||||
|
"python3-pynput",
|
||||||
|
"python3-pywhispercpp",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
#: Template options that switch off dependency verification. The app
|
||||||
|
#: resolves every dependency deliberately through the mapping above,
|
||||||
|
#: never by hiding it behind one of these.
|
||||||
|
FORBIDDEN_VARS = ("noverifypydeps", "nopyprovides")
|
||||||
|
|
||||||
|
|
||||||
|
def _template_text() -> str:
|
||||||
|
"""Read the raw template file."""
|
||||||
|
assert TEMPLATE.is_file(), f"missing Void app template: {TEMPLATE}"
|
||||||
|
return TEMPLATE.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _template_vars() -> dict:
|
||||||
|
"""Parse top-level VAR=value assignments from the template file."""
|
||||||
|
variables = {}
|
||||||
|
in_function = False
|
||||||
|
for line in _template_text().splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith("#"):
|
||||||
|
continue
|
||||||
|
if re.fullmatch(r"[A-Za-z0-9_+-]+\(\)\s*\{", stripped):
|
||||||
|
in_function = True
|
||||||
|
continue
|
||||||
|
if in_function:
|
||||||
|
if stripped == "}":
|
||||||
|
in_function = False
|
||||||
|
continue
|
||||||
|
key, separator, value = stripped.partition("=")
|
||||||
|
if not separator or not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key):
|
||||||
|
continue
|
||||||
|
value = value.strip()
|
||||||
|
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
||||||
|
value = value[1:-1]
|
||||||
|
variables[key] = value
|
||||||
|
return variables
|
||||||
|
|
||||||
|
|
||||||
|
def _code_lines() -> str:
|
||||||
|
"""Template with comment lines stripped; behaviour lives in code."""
|
||||||
|
return "\n".join(
|
||||||
|
line for line in _template_text().splitlines() if not line.lstrip().startswith("#")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _post_install_body() -> str:
|
||||||
|
"""Shell lines inside post_install(), without comments."""
|
||||||
|
lines = _template_text().splitlines()
|
||||||
|
start = next(i for i, line in enumerate(lines) if line.strip().startswith("post_install()"))
|
||||||
|
body = []
|
||||||
|
for line in lines[start + 1 :]:
|
||||||
|
if line.strip() == "}":
|
||||||
|
break
|
||||||
|
if line.strip() and not line.lstrip().startswith("#"):
|
||||||
|
body.append(line.strip())
|
||||||
|
return "\n".join(body)
|
||||||
|
|
||||||
|
|
||||||
|
def _app_version() -> str:
|
||||||
|
"""Version owned by src/vocalinux/version.py."""
|
||||||
|
match = re.search(r'^__version__ = "([^"]+)"', VERSION_PY.read_text(encoding="utf-8"), re.M)
|
||||||
|
assert match, "version.py no longer defines __version__"
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _project_dependencies() -> list:
|
||||||
|
"""Full dependency clauses from pyproject.toml."""
|
||||||
|
data = tomllib.loads(PYPROJECT.read_text(encoding="utf-8"))
|
||||||
|
return list(data["project"]["dependencies"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_template_lives_in_void_packages_layout() -> None:
|
||||||
|
"""The app template sits where xbps-src expects it: srcpkgs/<pkgname>."""
|
||||||
|
assert TEMPLATE.is_file(), f"{TEMPLATE} is missing"
|
||||||
|
assert INSTALL_MSG.is_file(), f"{INSTALL_MSG} is missing"
|
||||||
|
|
||||||
|
|
||||||
|
def test_pkgname_matches_template_directory() -> None:
|
||||||
|
"""Void requires pkgname to match srcpkgs/<pkgname>, using the new name."""
|
||||||
|
assert _template_vars()["pkgname"] == TEMPLATE.parent.name == "voculinux"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mandatory_void_variables_are_present() -> None:
|
||||||
|
"""The Manual's mandatory variables plus fetch inputs must be set."""
|
||||||
|
variables = _template_vars()
|
||||||
|
for key in (
|
||||||
|
"pkgname",
|
||||||
|
"version",
|
||||||
|
"revision",
|
||||||
|
"short_desc",
|
||||||
|
"maintainer",
|
||||||
|
"license",
|
||||||
|
"homepage",
|
||||||
|
"build_style",
|
||||||
|
"hostmakedepends",
|
||||||
|
"makedepends",
|
||||||
|
"depends",
|
||||||
|
"distfiles",
|
||||||
|
"checksum",
|
||||||
|
):
|
||||||
|
assert variables.get(key), f"template is missing {key}"
|
||||||
|
assert variables["build_style"] == "python3-module"
|
||||||
|
assert "python3-setuptools" in variables["hostmakedepends"].split()
|
||||||
|
assert "python3-devel" in variables["makedepends"].split()
|
||||||
|
assert variables["revision"] == "1", "new templates start at revision 1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_template_metadata_shape() -> None:
|
||||||
|
"""Maintainer, license, homepage and description follow the Manual."""
|
||||||
|
variables = _template_vars()
|
||||||
|
assert re.fullmatch(r".+ <[^@<>\s]+@[^@<>\s]+>", variables["maintainer"])
|
||||||
|
assert "noreply" not in variables["maintainer"]
|
||||||
|
assert variables["license"] == "AGPL-3.0-only"
|
||||||
|
assert variables["homepage"] == "https://github.com/VocaHQ/vocalinux"
|
||||||
|
assert len(variables["short_desc"]) <= 72
|
||||||
|
assert re.fullmatch(r"[0-9][A-Za-z0-9.]*", variables["version"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_template_version_matches_version_py() -> None:
|
||||||
|
"""The template builds the current source version, not a stale one."""
|
||||||
|
assert _template_vars()["version"] == _app_version()
|
||||||
|
|
||||||
|
|
||||||
|
def test_distfiles_sources_the_canonical_release_by_checksum() -> None:
|
||||||
|
"""One release-asset distfile, checksum-pinned; never a git snapshot."""
|
||||||
|
variables = _template_vars()
|
||||||
|
distfiles = variables["distfiles"]
|
||||||
|
version = variables["version"]
|
||||||
|
assert (
|
||||||
|
"https://github.com/VocaHQ/vocalinux/releases/download/v${version}/" in distfiles
|
||||||
|
), "distfiles must use the canonical release distribution for this version"
|
||||||
|
assert distfiles.rstrip().endswith(".tar.gz"), "the release payload is the sdist tarball"
|
||||||
|
for moving in ("/master", "/main", "/archive/", "git+"):
|
||||||
|
assert moving not in distfiles, f"distfiles must not track a moving ref: {moving}"
|
||||||
|
assert re.fullmatch(r"[0-9a-f]{64}", variables["checksum"]), "checksum must be sha256"
|
||||||
|
# The unpacked top-level directory must be where the template builds:
|
||||||
|
# wrksrc defaults to pkgname-version, so a distfile rooted under the
|
||||||
|
# pre-rename archive name needs an explicit wrksrc override.
|
||||||
|
root = Path(distfiles.rsplit("/", 1)[1].replace("${version}", version))
|
||||||
|
assert root.suffixes[-2:] == [".tar", ".gz"]
|
||||||
|
expected_wrksrc = variables.get("wrksrc", f"{variables['pkgname']}-${{version}}")
|
||||||
|
assert expected_wrksrc.replace("${version}", version) == root.name[: -len(".tar.gz")]
|
||||||
|
|
||||||
|
|
||||||
|
def test_every_project_dependency_is_covered() -> None:
|
||||||
|
"""pyproject.toml runtime deps plus substrate must all resolve to depends."""
|
||||||
|
depends = set(_template_vars()["depends"].split())
|
||||||
|
uncovered = []
|
||||||
|
for clause in _project_dependencies():
|
||||||
|
pip_name = re.match(r"^([A-Za-z0-9._-]+)", clause).group(1).lower().replace("_", "-")
|
||||||
|
void_name = PIP_TO_VOID.get(pip_name)
|
||||||
|
assert void_name, f"no Void mapping recorded for {pip_name}"
|
||||||
|
if void_name not in depends:
|
||||||
|
uncovered.append(f"{pip_name} -> {void_name}")
|
||||||
|
missing_extra = [name for name in EXTRA_VOID_DEPS if name not in depends]
|
||||||
|
assert not uncovered, f"deps with no Void coverage: {uncovered}"
|
||||||
|
assert not missing_extra, f"substrate missing from depends: {missing_extra}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_template_dependencies_exist_in_void_packages() -> None:
|
||||||
|
"""Template dependency lists name only verified void-packages names."""
|
||||||
|
variables = _template_vars()
|
||||||
|
referenced = set()
|
||||||
|
for key in ("depends", "hostmakedepends", "makedepends", "checkdepends"):
|
||||||
|
for token in variables.get(key, "").split():
|
||||||
|
if token.startswith("${"):
|
||||||
|
continue
|
||||||
|
referenced.add(token)
|
||||||
|
unknown = sorted(referenced - VERIFIED_VOID_PACKAGES)
|
||||||
|
assert not unknown, f"template references unverified packages: {unknown}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_stale_indicator_library_name() -> None:
|
||||||
|
"""The pre-rename libappindicator names do not exist in void-packages."""
|
||||||
|
variables = _template_vars()
|
||||||
|
for key in ("depends", "makedepends", "hostmakedepends"):
|
||||||
|
for token in variables.get(key, "").split():
|
||||||
|
assert token not in ("libappindicator", "libappindicator-gtk3"), (
|
||||||
|
f"{key} names {token}, which has no void-packages template; "
|
||||||
|
"the Ayatana fork (libayatana-appindicator) is the provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_pip_or_vendored_payload_at_build_or_install() -> None:
|
||||||
|
"""The build installs from the sdist; pip never runs, nothing is vendored."""
|
||||||
|
code = _code_lines().lower()
|
||||||
|
for forbidden in ("pip install", "pip3 install", "pip download", "requirements", ".whl"):
|
||||||
|
assert forbidden not in code, f"template runs pip or vendors wheels: {forbidden}"
|
||||||
|
assert "venv" not in code
|
||||||
|
body = _post_install_body().lower()
|
||||||
|
assert "pip" not in body, "post_install must not invoke pip"
|
||||||
|
|
||||||
|
|
||||||
|
def test_package_ships_no_speech_models() -> None:
|
||||||
|
"""Speech models download on first run; they are not package payload."""
|
||||||
|
code = _code_lines().lower()
|
||||||
|
assert "model" not in code, "models are not package payload"
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_install_uses_the_renamed_paths() -> None:
|
||||||
|
"""Desktop entry, icons, license and the packaged-install marker install."""
|
||||||
|
body = _post_install_body()
|
||||||
|
assert "vlicense LICENSE" in body
|
||||||
|
assert "voculinux.desktop" in body and "usr/share/applications" in body
|
||||||
|
assert "resources/icons/scalable" in body
|
||||||
|
assert "usr/share/icons/hicolor/scalable/apps" in body
|
||||||
|
assert "usr/share/voculinux/packaged" in body
|
||||||
|
assert "vocalinux" not in body, "post_install must use only the renamed install paths"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_fetches_nothing_beyond_the_sdist() -> None:
|
||||||
|
"""Only the homepage and the declared distfile may appear as URLs."""
|
||||||
|
text = _template_text()
|
||||||
|
assert text.count("https://") == 2, "homepage + distfiles are the only URLs a template needs"
|
||||||
|
assert "FetchContent" not in text and "ExternalProject" not in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_verification_disabling_options() -> None:
|
||||||
|
"""Dependency verification stays on; distfiles stay checksum-pinned."""
|
||||||
|
variables = _template_vars()
|
||||||
|
for key in FORBIDDEN_VARS:
|
||||||
|
assert key not in variables, f"template sets {key}"
|
||||||
|
assert re.fullmatch(r"[0-9a-f]{64}", variables["checksum"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_template_style_is_lint_clean() -> None:
|
||||||
|
"""Trailing whitespace and a missing final newline fail xlint."""
|
||||||
|
raw = TEMPLATE.read_bytes().decode("utf-8")
|
||||||
|
for lineno, line in enumerate(raw.splitlines(), 1):
|
||||||
|
assert line == line.rstrip(), f"trailing whitespace on line {lineno}"
|
||||||
|
assert "\t" not in line or line.startswith("\t"), f"stray tab on line {lineno}"
|
||||||
|
assert raw.endswith("\n"), "template must end with a newline"
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_msg_covers_the_xbps_only_facts() -> None:
|
||||||
|
"""INSTALL.msg tells the user what xbps cannot express."""
|
||||||
|
assert INSTALL_MSG.is_file(), f"missing install message: {INSTALL_MSG}"
|
||||||
|
text = INSTALL_MSG.read_text(encoding="utf-8").lower()
|
||||||
|
assert "input" in text, "the evdev hotkey backend needs the input group"
|
||||||
|
assert "model" in text, "models download on first run"
|
||||||
|
for engine in ("vosk", "whisper", "parakeet", "faster-whisper", "onnxruntime"):
|
||||||
|
assert engine in text, f"optional-engine status must name {engine}"
|
||||||
|
assert "ydotool" in text, "the absent injection helper must be named"
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_msg_line_length() -> None:
|
||||||
|
"""The Manual asks for at most 80 columns per line in INSTALL.msg."""
|
||||||
|
raw = INSTALL_MSG.read_bytes().decode("utf-8")
|
||||||
|
for lineno, line in enumerate(raw.splitlines(), 1):
|
||||||
|
assert len(line) <= 80, f"line {lineno} exceeds 80 columns"
|
||||||
|
assert raw.endswith("\n"), "INSTALL.msg must end with a newline"
|
||||||
|
|
||||||
|
|
||||||
|
def test_container_harness_covers_the_app_package() -> None:
|
||||||
|
"""build.sh stays podman-only; container-main smokes the installed app."""
|
||||||
|
build = BUILD_SH.read_text(encoding="utf-8")
|
||||||
|
assert "podman" in build, "the build harness must use podman"
|
||||||
|
assert "docker" not in build.lower().replace("never docker", "")
|
||||||
|
harness = CONTAINER_MAIN.read_text(encoding="utf-8")
|
||||||
|
assert "voculinux --version" in harness, "the app smoke checks the entry point"
|
||||||
|
assert "usr/share/applications/voculinux.desktop" in harness
|
||||||
|
assert "usr/share/icons/hicolor/scalable/apps/voculinux.svg" in harness
|
||||||
|
assert "usr/share/voculinux/packaged" in harness
|
||||||
|
assert "python3-pywhispercpp" in harness or "PKGNAME" in harness
|
||||||
|
|
||||||
|
|
||||||
|
def test_void_distro_inventory_matches_void_packages_reality() -> None:
|
||||||
|
"""The installer inventory names the Ayatana provider and marks ydotool absent."""
|
||||||
|
text = DISTRO_MAP.read_text(encoding="utf-8")
|
||||||
|
void_block = text.split(" void:", 1)[1].split("\n solus:", 1)[0]
|
||||||
|
code = "\n".join(line for line in void_block.splitlines() if not line.lstrip().startswith("#"))
|
||||||
|
assert "libayatana-appindicator" in code
|
||||||
|
assert "libappindicator-gtk3" not in code
|
||||||
|
assert "libappindicator " not in code.replace("libayatana-appindicator", "")
|
||||||
|
assert "absent" in void_block, "the ydotool absence must be marked in the inventory"
|
||||||
|
|
||||||
|
|
||||||
|
def test_void_install_docs_name_the_ayatana_provider() -> None:
|
||||||
|
"""The Void install snippet must install a package that exists."""
|
||||||
|
text = DISTRO_DOC.read_text(encoding="utf-8")
|
||||||
|
void_section = text.split("### Void Linux", 1)[1].split("###", 1)[0]
|
||||||
|
assert "libayatana-appindicator" in void_section
|
||||||
|
assert "libappindicator-gtk3" not in void_section
|
||||||
|
assert "libappindicator " not in void_section.replace("libayatana-appindicator", "")
|
||||||
Reference in New Issue
Block a user