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
This commit is contained in:
@@ -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,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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user