fix(void): carry python3-pysocks and constrain the audio and SOCKS floors
CI / security (pull_request) Successful in 1m13s
CI / lint (pull_request) Successful in 23s
CI / ai-review (pull_request) Successful in 8s

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
This commit is contained in:
2026-11-06 21:32:33 +05:30
parent b47a65c91e
commit 06b7b79664
10 changed files with 240 additions and 38 deletions
+68 -5
View File
@@ -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>=<carried version>_<revision>. 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<name>[A-Za-z0-9+._-]+)(?:(?P<op>>=|<=|>|<)(?P<version>[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/<pkgname>."""
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>=<carried pkgver>, 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()
+1 -1
View File
@@ -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
+41 -18
View File
@@ -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/<pkgname>."""
"""Every carried template sits where xbps-src expects it: srcpkgs/<pkgname>."""
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:
+29 -1
View File
@@ -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: