From 06b7b79664cfdbf567f16144d0be99abbb4ce33f Mon Sep 17 00:00:00 2001 From: Xavier Karma Date: Fri, 6 Nov 2026 21:32:33 +0530 Subject: [PATCH] fix(void): carry python3-pysocks and constrain the audio and SOCKS floors Void's stock python3-PyAudio (0.2.12_5) and python3-pysocks (1.7.0_8) sit below the project floors (pyaudio>=0.2.13, PySocks>=1.7.1). voculinux depended on both by bare name, which an installed stock package already satisfies, so a user who had the stock packages was never upgraded. - add packaging/void/srcpkgs/python3-pysocks: PySocks 1.7.1 from the PyPI sdist that uv.lock pins (sha256 3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0) - voculinux depends on python3-PyAudio>=0.2.14_1 and python3-pysocks>=1.7.1_1 - the install smoke installs the stock packages first, then voculinux, and fails unless each reads back as its carried pkgver - tests: each constraint must name its carried template's current version and revision; the stock version must sit below the floor; the depends parser refuses malformed tokens instead of stripping them - docs: "Version floors" in packaging/void/README.md; a pointer in docs/RELEASE_PROCESS.md Refs #16 --- docs/RELEASE_PROCESS.md | 8 ++ packaging/void/README.md | 60 ++++++++++++--- packaging/void/container-release.sh | 17 ++++- packaging/void/smoke.sh | 2 +- .../void/srcpkgs/python3-pysocks/template | 20 +++++ packaging/void/srcpkgs/voculinux/template | 7 +- tests/test_void_app_template.py | 73 +++++++++++++++++-- tests/test_void_pywhispercpp_template.py | 2 +- tests/test_void_small_deps.py | 59 ++++++++++----- tests/test_void_smoke_gate.py | 30 +++++++- 10 files changed, 240 insertions(+), 38 deletions(-) create mode 100644 packaging/void/srcpkgs/python3-pysocks/template diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index 5baf60c..4e54a6d 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -57,6 +57,14 @@ is not `repository-key.plist`. The private key is never committed; only the public `.plist` is. Key generation and the check that a private key matches the committed public key are in `packaging/void/README.md`. +The carried Void templates in `packaging/void/srcpkgs/` track the project's own +pins. `python3-PyAudio` and `python3-pysocks` replace stock Void packages below +the floors in `pyproject.toml`, and `voculinux` depends on each with a `>=` +constraint. When a floor or a carried version changes, change the template and +its constraint in the same pull request: the offline tests in +`tests/test_void_small_deps.py` and `tests/test_void_app_template.py` fail +until they agree. The reasoning is in `packaging/void/README.md`, "Version floors". + ## Quick Release Checklist Use this checklist for every release: diff --git a/packaging/void/README.md b/packaging/void/README.md index 1b68eeb..1351f91 100644 --- a/packaging/void/README.md +++ b/packaging/void/README.md @@ -21,6 +21,7 @@ packaging/void/ ├── python3-pywhispercpp/ ticket #26: whisper.cpp Python binding ├── python3-pynput/ ticket #25: the input library Void does not ship ├── python3-PyAudio/ ticket #25: the audio binding, updated past Void's copy + ├── python3-pysocks/ spec #16 gap: the SOCKS module, updated past Void's copy └── voculinux ticket #27: the application ``` @@ -33,9 +34,45 @@ packaging/void/ | Template | Ticket | Notes | |---|---|---| | `python3-pywhispercpp` | #26 | Vulkan backend on by default; see below | -| `python3-pynput`, `python3-PyAudio` | #25 | Dependencies Void does not ship at the needed version | +| `python3-pynput` | #25 | A dependency Void does not ship | +| `python3-PyAudio`, `python3-pysocks` | spec #16 gap | Void's copies are below the project floors; see [Version floors](#version-floors) | | `voculinux` | #27 | The app, from the canonical release sdist; see below | +## Version floors + +`pyproject.toml` declares floors that Void's own packages do not meet. On +2026-11-06 the stock packages on void-packages master were: + +| Package | Stock Void | Project floor | Carried template | `voculinux` depends | +|---|---|---|---|---| +| `python3-PyAudio` | 0.2.12_5 | `pyaudio>=0.2.13` | 0.2.14_1 | `python3-PyAudio>=0.2.14_1` | +| `python3-pysocks` | 1.7.0_8 | `PySocks>=1.7.1,<2` | 1.7.1_1 | `python3-pysocks>=1.7.1_1` | + +The gap is closed in the personal repository, not hidden. The carried +templates build the versions `uv.lock` pins, and `voculinux` names each one +with a `>=` constraint in the xbps `name>=version_revision` form (the xbps-src +manual lists `<`, `>`, `<=` and `>=`). A bare name would be satisfied by the +stock package a user already has, so xbps would leave it in place and the app +would run below its floor. With the constraint, xbps takes the carried +template from the signed repository instead. Dependency verification stays on. + +The install smoke checks the upgrade, not just a clean install: it installs +the stock packages from Void first, then `voculinux`, and fails unless each +package reads back as its carried pkgver. +`tests/test_void_app_template.py` requires every constraint to name its +template's current version and revision, so a template bump without a +`depends` bump fails offline. `tests/test_void_small_deps.py` requires the +stock version below the floor and the carried version at or above it. + +Known limit: xbps has one comparator per dependency, so PySocks' `<2` cannot +be written. The carried 1.7.1 satisfies it. A future Void `python3-pysocks` +2.x would also satisfy `>=1.7.1_1`, so it is not excluded by the dependency. + +When a project floor rises, or Void's stock package reaches the floor, change +the carried template and its constraint in the same pull request. The +stock-below-floor test fails once Void catches up, which is the signal to +retire the carried template deliberately. + ## python3-pywhispercpp Version `1.5.0`, pinned to the same version `install.sh` @@ -70,9 +107,11 @@ upstream. Read [Version mapping](#version-mapping) and [Checksum](#checksum) before changing `version`, `_tag` or `checksum`. - **Runtime deps** are Void's own interpreter, GTK and desktop bindings, - the packaged Python libraries, the three templates from tickets #25 - and #26 (`python3-pynput`, `python3-PyAudio`, `python3-pywhispercpp`), - and the helper tools the app shells out to: PipeWire (capture), `wtype` + the packaged Python libraries, the carried templates (`python3-pynput`, + `python3-PyAudio`, `python3-pysocks` and `python3-pywhispercpp`; the audio + binding and SOCKS module are version-constrained, see + [Version floors](#version-floors)), and the helper tools the app shells + out to: PipeWire (capture), `wtype` (direct typing on niri), `wl-clipboard`, `xdotool` and `pulseaudio-utils`. `ibus` is not a dependency: it cannot inject on niri. Every name was verified to exist in current void-packages; the @@ -156,11 +195,14 @@ host Void install and nothing is published. Stages, in order, each fails the run with `xlint`, built with `xbps-src`, the sdist resolved by `xbps-src show`; 4. `sign`: the packages indexed and signed, with no network; 5. `install`: in a clean image with no key, an unsigned install is refused, the - signed repository is trusted, `voculinux` installs, `voculinux --version` - shows the version, the payload checks pass (desktop entry, icon, marker, no - models, no pre-rename names, the Vulkan backend, no host-native CPU tuning), - and the imports load (`pynput` through its dummy backend, `evdev`, `gi` with - Gtk 3, `pywhispercpp.model`, `pyaudio`, and the app's own modules, which keep their retained internal name). + signed repository is trusted, Void's stock `python3-PyAudio` and + `python3-pysocks` are installed, then `voculinux` installs and upgrades both + to the carried versions (see [Version floors](#version-floors)), + `voculinux --version` shows the version, the payload checks pass (desktop + entry, icon, marker, no models, no pre-rename names, the Vulkan backend, no + host-native CPU tuning), and the imports load (`pynput` through its dummy + backend, `evdev`, `gi` with Gtk 3, `pywhispercpp.model`, `pyaudio`, `socks`, + and the app's own modules, which keep their retained internal name). `tests/test_void_smoke_gate.py` asserts offline that the gate stays wired to every `srcpkgs` template. diff --git a/packaging/void/container-release.sh b/packaging/void/container-release.sh index a185c6b..c0556d6 100755 --- a/packaging/void/container-release.sh +++ b/packaging/void/container-release.sh @@ -137,12 +137,24 @@ phase_install() { xbps-query voculinux >/dev/null 2>&1 && fail "voculinux was installed by the untrusted attempt" echo "refused without the key, as required (rc=$REFUSAL_RC)" - echo "== trust the staged key, then install voculinux from the signed repository" + echo "== trust the staged key" cp "$XBPS_STAGE"/keys/*.plist /var/db/xbps/keys/ + + echo "== stock Void packages below the project floors, as a user who has them already" + xbps-install -y --repository="$OFFICIAL" -S python3-PyAudio python3-pysocks >/dev/null + + echo "== install voculinux from the signed repository; the floors must upgrade the stock packages" xbps-install -y --repository="$XBPS_STAGE/repo" --repository="$OFFICIAL" -S voculinux >/dev/null xbps-query voculinux | grep -qx "pkgver: voculinux-${XBPS_VERSION}_1" || fail "installed package is not voculinux-${XBPS_VERSION}_1" echo "installed: voculinux-${XBPS_VERSION}_1" + for PKG in python3-PyAudio python3-pysocks; do + PKGFILE=$(ls "$PKG_STAGE"/"$PKG"-[0-9]*.x86_64.xbps | head -n1) + PKGVER=$(basename "$PKGFILE" .x86_64.xbps) + xbps-query "$PKG" | grep -qx "pkgver: $PKGVER" || + fail "$PKG is not $PKGVER after voculinux: the stock package was not upgraded" + echo "upgraded to the carried template: $PKGVER" + done echo "== the renamed binary reports its version" VERSION_OUT=$(voculinux --version 2>&1) || fail "voculinux --version exited non-zero" @@ -155,7 +167,7 @@ phase_install() { case "$PKG" in voculinux) payload_voculinux "$PKGFILE" ;; python3-pywhispercpp) payload_pywhispercpp "$PKGFILE" ;; - python3-pynput | python3-PyAudio) ;; # checked by import below + python3-pynput | python3-PyAudio | python3-pysocks) ;; # checked by import below *) fail "no smoke recipe for package $PKG; add one alongside its template" ;; esac done @@ -168,6 +180,7 @@ phase_install() { python3 -c "import gi; gi.require_version('Gtk', '3.0'); from gi.repository import Gtk; print('gi ok: Gtk', Gtk.get_major_version(), Gtk.get_minor_version())" python3 -c "from pywhispercpp.model import Model; print('pywhispercpp.model ok')" python3 -c "import pyaudio; print('pyaudio ok')" + python3 -c "import socks; print('pysocks ok')" python3 -c "import vocalinux.main, vocalinux.speech_recognition.recognition_manager; print('vocalinux imports ok')" echo "INSTALL SMOKE OK: voculinux $VERSION installed from the signed repository" } diff --git a/packaging/void/smoke.sh b/packaging/void/smoke.sh index 4dac15c..7e554a1 100755 --- a/packaging/void/smoke.sh +++ b/packaging/void/smoke.sh @@ -20,6 +20,6 @@ HERE=$(cd -- "$HERE" && pwd) # Dependency order: the app last. tests/test_void_smoke_gate.py fails if this # list drifts from packaging/void/srcpkgs. # shellcheck disable=SC2034 # read as data by build-release.sh, which parses this line -PKGS="python3-PyAudio python3-pynput python3-pywhispercpp voculinux" +PKGS="python3-PyAudio python3-pynput python3-pysocks python3-pywhispercpp voculinux" exec "$HERE/build-release.sh" smoke diff --git a/packaging/void/srcpkgs/python3-pysocks/template b/packaging/void/srcpkgs/python3-pysocks/template new file mode 100644 index 0000000..0c0ce56 --- /dev/null +++ b/packaging/void/srcpkgs/python3-pysocks/template @@ -0,0 +1,20 @@ +# Template file for 'python3-pysocks' +# Carried for the personal repository: Void's python3-pysocks is 1.7.0, below +# the project floor PySocks>=1.7.1 (pyproject.toml). The version and checksum +# are the PyPI sdist that uv.lock pins. See packaging/void/README.md. +pkgname=python3-pysocks +version=1.7.1 +revision=1 +build_style=python3-module +hostmakedepends="python3-setuptools" +depends="python3" +short_desc="Python3 SOCKS client module" +maintainer="Orphaned " +license="BSD-3-Clause" +homepage="https://github.com/Anorov/PySocks" +distfiles="${PYPI_SITE}/P/PySocks/PySocks-${version}.tar.gz" +checksum=3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0 + +post_install() { + vlicense LICENSE +} diff --git a/packaging/void/srcpkgs/voculinux/template b/packaging/void/srcpkgs/voculinux/template index ffdf176..dc71bb8 100644 --- a/packaging/void/srcpkgs/voculinux/template +++ b/packaging/void/srcpkgs/voculinux/template @@ -15,7 +15,12 @@ makedepends="python3-devel" # direct typing on niri, wl-clipboard (wl-copy, wl-paste) for clipboard # paths, xdotool for X11 and the XWayland fallback, pulseaudio-utils for # pactl source listing. ibus is not a dependency: it is excluded on niri. -depends="python3 python3-gobject python3-cairo gtk+3 libayatana-appindicator gobject-introspection python3-numpy python3-requests python3-pysocks python3-psutil python3-evdev python3-xlib python3-tqdm python3-platformdirs python3-PyAudio python3-pynput python3-pywhispercpp pipewire wtype wl-clipboard xdotool pulseaudio-utils" +# python3-PyAudio and python3-pysocks are version-constrained. Void's stock +# packages (0.2.12_5, 1.7.0_8) sit below the project floors, and a bare name is +# already satisfied by a stock package a user has installed, so it would never +# be upgraded to the carried template. Each >= names the carried template's +# version and revision; tests/test_void_app_template.py keeps them in step. +depends="python3 python3-gobject python3-cairo gtk+3 libayatana-appindicator gobject-introspection python3-numpy python3-requests python3-pysocks>=1.7.1_1 python3-psutil python3-evdev python3-xlib python3-tqdm python3-platformdirs python3-PyAudio>=0.2.14_1 python3-pynput python3-pywhispercpp pipewire wtype wl-clipboard xdotool pulseaudio-utils" short_desc="Free, offline voice dictation for Linux" maintainer="Xavier Karma " license="AGPL-3.0-only" diff --git a/tests/test_void_app_template.py b/tests/test_void_app_template.py index 2d4cae4..8aef76e 100644 --- a/tests/test_void_app_template.py +++ b/tests/test_void_app_template.py @@ -23,6 +23,8 @@ import re import tomllib from pathlib import Path +import pytest + REPO_ROOT = Path(__file__).resolve().parents[1] VOID = REPO_ROOT / "packaging" / "void" SRC = VOID / "srcpkgs" @@ -128,6 +130,19 @@ VERIFIED_VOID_PACKAGES = frozenset( #: never by hiding it behind one of these. FORBIDDEN_VARS = ("noverifypydeps", "nopyprovides") +#: Carried templates whose stock Void package sits below the project floor +#: (tests/test_void_small_deps.py, GAP_PACKAGES). voculinux must require each +#: one as name>=_. A bare name is satisfied by the +#: stock package a user already has, so it would never be upgraded. +GAP_DEPENDS = ("python3-PyAudio", "python3-pysocks") + +#: One xbps dependency token: a bare name, or name, comparator and +#: version_revision. The whole token must match; a malformed token such as +#: "python3-PyAudio>=" is refused, never stripped down to its name. +DEPENDS_TOKEN = re.compile( + r"(?P[A-Za-z0-9+._-]+)(?:(?P>=|<=|>|<)(?P[0-9][A-Za-z0-9.]*_[0-9]+))?" +) + def _template_text() -> str: """Read the raw template file.""" @@ -193,6 +208,31 @@ def _project_dependencies() -> list: return list(data["project"]["dependencies"]) +def _depends_tokens(value: str) -> list: + """Validated tokens of an xbps depends value; a malformed token fails the test.""" + tokens = [] + for token in value.split(): + if token.startswith("${"): + continue + assert DEPENDS_TOKEN.fullmatch(token), f"malformed dependency token: {token!r}" + tokens.append(token) + return tokens + + +def _depends_names(value: str) -> set: + """Package names of an xbps depends value, with any version constraint removed.""" + return {DEPENDS_TOKEN.fullmatch(token).group("name") for token in _depends_tokens(value)} + + +def _carried_pkgver(name: str) -> str: + """The pkgver a carried template builds: its version and revision, as written.""" + text = (SRC / name / "template").read_text(encoding="utf-8") + version = re.search(r"^version=(\S+)$", text, re.M) + revision = re.search(r"^revision=(\d+)$", text, re.M) + assert version and revision, f"{name} template lacks a version or revision" + return f"{version.group(1)}_{revision.group(1)}" + + def test_app_template_lives_in_void_packages_layout() -> None: """The app template sits where xbps-src expects it: srcpkgs/.""" assert TEMPLATE.is_file(), f"{TEMPLATE} is missing" @@ -275,7 +315,7 @@ def test_checksum_is_a_recorded_digest_or_the_build_placeholder() -> None: 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()) + depends = _depends_names(_template_vars()["depends"]) uncovered = [] for clause in _project_dependencies(): pip_name = re.match(r"^([A-Za-z0-9._-]+)", clause).group(1).lower().replace("_", "-") @@ -293,14 +333,37 @@ def test_template_dependencies_exist_in_void_packages() -> None: 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) + referenced |= _depends_names(variables.get(key, "")) unknown = sorted(referenced - VERIFIED_VOID_PACKAGES) assert not unknown, f"template references unverified packages: {unknown}" +def test_depends_tokens_are_validated_not_stripped() -> None: + """A constrained token is accepted only in full; a truncated one is malformed.""" + assert DEPENDS_TOKEN.fullmatch("python3-PyAudio>=0.2.14_1") + assert DEPENDS_TOKEN.fullmatch("python3-pysocks") + for malformed in ("python3-PyAudio>=", "python3-PyAudio>=0.2.14", ">=0.2.14_1"): + assert not DEPENDS_TOKEN.fullmatch(malformed), malformed + with pytest.raises(AssertionError): + _depends_tokens(malformed) + + +def test_gap_packages_are_version_constrained_to_their_carried_templates() -> None: + """voculinux requires name>=, never the bare name. + + A bare name is satisfied by the stock package a user already has (below the + floor), so it would keep the stock package. The constraint must track the + carried template: bumping its version or revision without this line fails. + """ + tokens = _depends_tokens(_template_vars()["depends"]) + for name in GAP_DEPENDS: + matching = [ + token for token in tokens if DEPENDS_TOKEN.fullmatch(token).group("name") == name + ] + expected = f"{name}>={_carried_pkgver(name)}" + assert matching == [expected], f"depends must name {expected}; found {matching}" + + def test_no_stale_indicator_library_name() -> None: """The pre-rename libappindicator names do not exist in void-packages.""" variables = _template_vars() diff --git a/tests/test_void_pywhispercpp_template.py b/tests/test_void_pywhispercpp_template.py index d69a6cb..7a4f40e 100644 --- a/tests/test_void_pywhispercpp_template.py +++ b/tests/test_void_pywhispercpp_template.py @@ -1,6 +1,6 @@ """Offline policy tests for the Void template of the whisper.cpp binding (#26). -Spec #16 packages three templates in void-packages layout under +Spec #16 packages four carried templates in void-packages layout under ``packaging/void``; this ticket owns the hard one, ``python3-pywhispercpp``, built from the pinned PyPI source distribution with the Vulkan backend, shared libraries, and portable CPU flags. Ticket #28 builds the container diff --git a/tests/test_void_small_deps.py b/tests/test_void_small_deps.py index 7963d2f..82dec2b 100644 --- a/tests/test_void_small_deps.py +++ b/tests/test_void_small_deps.py @@ -1,10 +1,10 @@ """Offline policy guards for the Void small-dependency templates (ticket #25). -Spec #16 (seam 2) closes the two easy Void dependency gaps — the X11 -keyboard input library and the updated audio binding — as in-repo xbps -templates in void-packages layout under the packaging tree, with the audio -binding at or above the project's declared floor and no -verification-disabling template options anywhere. +Spec #16 (seam 2) closes the Void dependency gaps — the X11 keyboard input +library, and the audio binding and SOCKS module that sit below the project's +declared floors — as in-repo xbps templates in void-packages layout under the +packaging tree, each at or above its floor, and no verification-disabling +template options anywhere. These tests pin the templates to the project's own lockfile (`uv.lock`, `requirements/runtime.txt`, `pyproject.toml`) so a template that drifts @@ -17,6 +17,8 @@ import re import tomllib from pathlib import Path +import pytest + REPO_ROOT = Path(__file__).resolve().parents[1] VOID = REPO_ROOT / "packaging" / "void" SRC = VOID / "srcpkgs" @@ -25,10 +27,24 @@ PYPROJECT = REPO_ROOT / "pyproject.toml" RUNTIME = REPO_ROOT / "requirements" / "runtime.txt" LOCK = REPO_ROOT / "uv.lock" -TEMPLATES = ("python3-pynput", "python3-PyAudio") +TEMPLATES = ("python3-pynput", "python3-PyAudio", "python3-pysocks") #: uv.lock package name per template directory name. -LOCK_NAMES = {"python3-pynput": "pynput", "python3-PyAudio": "pyaudio"} +LOCK_NAMES = { + "python3-pynput": "pynput", + "python3-PyAudio": "pyaudio", + "python3-pysocks": "pysocks", +} + +#: Carried templates that replace a stock Void package sitting below the +#: project's floor: template -> (pip name, stock Void pkgver, the floor). +#: The stock pkgvers were read from void-packages master on 2026-11-06 +#: (srcpkgs/python3-PyAudio/template 0.2.12_5, srcpkgs/python3-pysocks/template +#: 1.7.0_8). Stock below the floor is what makes the gap real. +GAP_PACKAGES = { + "python3-PyAudio": ("pyaudio", "0.2.12_5", (0, 2, 13)), + "python3-pysocks": ("pysocks", "1.7.0_8", (1, 7, 1)), +} #: Void package names covering pynput's linux-marked install_requires #: (evdev>=1.3, python-xlib>=0.17, six) as recorded in uv.lock. @@ -62,6 +78,7 @@ FORBIDDEN_VARS = ("noverifypydeps", "nopyprovides") EXPECTED_LICENSES = { "python3-pynput": "LGPL-3.0-or-later", "python3-PyAudio": "MIT", + "python3-pysocks": "BSD-3-Clause", } #: Void Manual mandatory variables, plus the fetch inputs every template @@ -165,7 +182,7 @@ def _void_pins() -> dict: def test_templates_live_in_void_packages_layout() -> None: - """Both templates sit where xbps-src expects them: srcpkgs/.""" + """Every carried template sits where xbps-src expects it: srcpkgs/.""" for name in TEMPLATES: template = SRC / name / "template" assert template.is_file(), f"{template} is missing" @@ -203,10 +220,12 @@ def test_template_checksums_match_lock_sdist() -> None: assert _template_vars(name)["checksum"] == digest, name -def test_audio_binding_satisfies_project_floor() -> None: +@pytest.mark.parametrize("name", sorted(GAP_PACKAGES)) +def test_carried_templates_satisfy_project_floors(name: str) -> None: """The updated template meets the declared floor it was built to close.""" - specifier = _project_specifier("pyaudio") - template = _version_tuple(_template_version("python3-PyAudio")) + pip_name, _, _ = GAP_PACKAGES[name] + specifier = _project_specifier(pip_name) + template = _version_tuple(_template_version(name)) for operator, floor in re.findall(r"(>=|==|~=|>)\s*([\d][\d.]*)", specifier): if operator == ">": assert template > _version_tuple(floor), specifier @@ -214,16 +233,20 @@ def test_audio_binding_satisfies_project_floor() -> None: assert template >= _version_tuple(floor), specifier -def test_audio_binding_floor_was_not_relaxed() -> None: - """Void's 0.2.12 stays below the floor: the floor itself never moved.""" - specifier = _project_specifier("pyaudio") +@pytest.mark.parametrize("name", sorted(GAP_PACKAGES)) +def test_stock_package_is_below_the_floor_and_the_floor_was_not_relaxed(name: str) -> None: + """Stock Void sits below the floor, so the gap is real; the floor never moved.""" + pip_name, stock, floor = GAP_PACKAGES[name] + stock_version = _version_tuple(stock.split("_")[0]) + assert stock_version < floor, f"{name}: stock {stock} is not below the floor {floor}" + specifier = _project_specifier(pip_name) lowers = [ - _version_tuple(floor) - for operator, floor in re.findall(r"(>=|==|~=|>)\s*([\d][\d.]*)", specifier) + _version_tuple(bound) + for operator, bound in re.findall(r"(>=|==|~=|>)\s*([\d][\d.]*)", specifier) if operator in (">=", "==", "~=", ">") ] - assert lowers, f"pyaudio clause carries no lower bound: {specifier}" - assert min(lowers) >= (0, 2, 13), f"floor relaxed to hide the gap: {specifier}" + assert lowers, f"{pip_name} clause carries no lower bound: {specifier}" + assert min(lowers) >= floor, f"floor relaxed to hide the gap: {specifier}" def test_no_verification_disabling_options() -> None: diff --git a/tests/test_void_smoke_gate.py b/tests/test_void_smoke_gate.py index 4d4088c..112eb3a 100644 --- a/tests/test_void_smoke_gate.py +++ b/tests/test_void_smoke_gate.py @@ -100,10 +100,38 @@ def test_the_ggml_scan_belongs_to_the_whisper_binding_only() -> None: def test_the_leaf_packages_are_imported_headless() -> None: - """PyAudio and pynput are proven by import: pynput's dummy backend needs no display.""" + """PyAudio, pynput and pysocks are proven by import: pynput's dummy backend needs no display.""" text = CONTAINER_RELEASE.read_text(encoding="utf-8") assert "import pyaudio" in text assert "PYNPUT_BACKEND=dummy" in text and "import pynput.keyboard" in text + assert "import socks" in text, "pysocks is proven by import" + + +def test_every_gate_package_has_an_install_recipe() -> None: + """Each package the gate builds has a label of its own in the per-package case.""" + text = CONTAINER_RELEASE.read_text(encoding="utf-8") + start = text.index('case "$PKG" in') + block = text[start : text.index("esac", start)] + labels = set() + for match in re.finditer(r"^\s*([^)\n]+)\)", block, re.M): + labels.update(part.strip() for part in match.group(1).split("|")) + for name in _gate_packages(): + assert name in labels, f"container-release.sh has no install recipe for {name}" + + +def test_install_smoke_upgrades_the_stock_packages_below_the_floors() -> None: + """A user who already has Void's stock audio binding and SOCKS module must get + the carried ones: the smoke installs the stock packages first, then voculinux, + then checks each one reads back as the carried pkgver.""" + text = CONTAINER_RELEASE.read_text(encoding="utf-8") + stock = 'xbps-install -y --repository="$OFFICIAL" -S python3-PyAudio python3-pysocks' + assert stock in text, "the stock packages must be installed before voculinux" + assert text.index(stock) < text.index( + 'xbps-install -y --repository="$XBPS_STAGE/repo" --repository="$OFFICIAL" -S voculinux' + ), "the stock install must precede the voculinux install" + assert ( + 'xbps-query "$PKG" | grep -qx "pkgver: $PKGVER"' in text + ), "each stock package must read back as the carried pkgver" def test_a_package_without_a_smoke_recipe_fails_the_release() -> None: