From 06b7b79664cfdbf567f16144d0be99abbb4ce33f Mon Sep 17 00:00:00 2001 From: Xavier Karma Date: Fri, 6 Nov 2026 21:32:33 +0530 Subject: [PATCH 1/2] 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: -- 2.54.0 From 4a0a245578fd120fb0f7fefaa387655e154b4ec1 Mon Sep 17 00:00:00 2001 From: Xavier Karma Date: Fri, 6 Nov 2026 21:45:04 +0530 Subject: [PATCH 2/2] test(void): tie the stock pre-install to the constrained depends The version-constrained carried packages are named in three places: the voculinux depends, the stock pre-install line and the pkgver loop of the install smoke. Nothing required them to agree, so a third gap package could be added to one place and missed in the others. The new test requires all three to name the same set. Also corrects the README "Version floors" wording: the offline stock-version test only records the versions verified on 2026-11-06, and the install smoke's pkgver check is the live signal. Refs #16 --- packaging/void/README.md | 11 +++++++---- tests/test_void_smoke_gate.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packaging/void/README.md b/packaging/void/README.md index 1351f91..30d825a 100644 --- a/packaging/void/README.md +++ b/packaging/void/README.md @@ -68,10 +68,13 @@ 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. +When a project floor rises, or Void's stock package moves up to the carried +version, change the carried template and its constraint in the same pull +request. The live signal is the install smoke: once the stock package reaches +the carried version, the constraint is already met, the stock package is kept, +and the pkgver check fails. That is the signal to retire the carried template +deliberately. The offline stock-version test in `tests/test_void_small_deps.py` +only records the versions verified on 2026-11-06; it does not query Void. ## python3-pywhispercpp diff --git a/tests/test_void_smoke_gate.py b/tests/test_void_smoke_gate.py index 112eb3a..209120c 100644 --- a/tests/test_void_smoke_gate.py +++ b/tests/test_void_smoke_gate.py @@ -134,6 +134,23 @@ def test_install_smoke_upgrades_the_stock_packages_below_the_floors() -> None: ), "each stock package must read back as the carried pkgver" +def test_stock_preinstall_matches_the_constrained_depends() -> None: + """A carried dependency that voculinux version-constrains must be installed as + stock first and checked by pkgver. Three places name the set: the voculinux + depends, the stock pre-install line and the pkgver loop. They must agree, so a + third gap package cannot be missed in one of them.""" + template = (SRC / "voculinux" / "template").read_text(encoding="utf-8") + depends = re.search(r'^depends="([^"]+)"', template, re.M).group(1).split() + constrained = {token.split(">=")[0] for token in depends if ">=" in token} + text = CONTAINER_RELEASE.read_text(encoding="utf-8") + stock = re.search(r'--repository="\$OFFICIAL" -S ([^>\n]+?) >/dev/null', text).group(1).split() + checked = re.search(r"for PKG in (python3-[^;]+); do", text).group(1).split() + assert set(stock) == constrained == set(checked), ( + f"stock pre-install {stock}, constrained depends {sorted(constrained)}, " + f"pkgver loop {checked} must name the same packages" + ) + + def test_a_package_without_a_smoke_recipe_fails_the_release() -> None: """A future template falls into *) and must fail loudly, never pass mute.""" text = CONTAINER_RELEASE.read_text(encoding="utf-8") -- 2.54.0