"""Offline policy guards for the Void small-dependency templates (ticket #25). 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 from the locked versions, relaxes the floor, disables dependency verification, or names a package that does not exist in void-packages fails offline — the same contract as the native/AppImage policy tests. """ 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" PINS = VOID / "tool_checksums.txt" PYPROJECT = REPO_ROOT / "pyproject.toml" RUNTIME = REPO_ROOT / "requirements" / "runtime.txt" LOCK = REPO_ROOT / "uv.lock" TEMPLATES = ("python3-pynput", "python3-PyAudio", "python3-pysocks") #: uv.lock package name per template directory name. 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. PYNPUT_LINUX_DEPS = ("python3-evdev", "python3-six", "python3-xlib") #: Every package name the templates may reference, verified to exist in #: void-packages master on 2026-11-05: python3-{evdev,six,xlib} and #: python3-setuptools templates fetched and read; portaudio-devel and #: python3-devel confirmed as subpackages of the portaudio/python3 #: templates. Re-verify with `xbps-query -Rs ` on a Void system #: (or the srcpkgs tree on GitHub) before extending this set. VERIFIED_VOID_PACKAGES = frozenset( { "python3", "python3-devel", "python3-setuptools", "python3-evdev", "python3-six", "python3-xlib", "portaudio-devel", } ) #: Template options that switch off dependency verification. Ticket #25 #: resolves the audio version gap deliberately with an updated template, #: never by hiding it behind one of these. FORBIDDEN_VARS = ("noverifypydeps", "nopyprovides") #: SPDX identifiers for the pinned versions (Void Manual: license must be #: an SPDX expression). pynput 1.8.2 declares LGPLv3; PyAudio is MIT. 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 #: here must declare so nothing arrives from an unpinned URL. MANDATORY_VARS = ( "pkgname", "version", "revision", "short_desc", "maintainer", "license", "homepage", "build_style", "hostmakedepends", "distfiles", "checksum", ) def _template_vars(name: str) -> dict: """Parse top-level VAR=value assignments from a void template file.""" text = (SRC / name / "template").read_text(encoding="utf-8") variables = {} in_function = False for line in 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 _locked_sdist(name: str) -> tuple: """Return (version, sdist sha256) for a package in uv.lock.""" text = LOCK.read_text(encoding="utf-8") block = re.search( rf'^name = "{re.escape(name)}"\nversion = "([^"]+)"(.*?)(?=^\[\[package\]\]|\Z)', text, re.M | re.S, ) assert block, f"{name} is missing from uv.lock" sdist = re.search(r'sdist = \{[^}]*hash = "sha256:([0-9a-f]{64})"', block.group(2)) assert sdist, f"{name} has no hashed sdist in uv.lock" return block.group(1), sdist.group(1) def _exported_version(name: str) -> str: """Return the pinned version from the hash-pinned runtime export.""" match = re.search( rf"^{re.escape(name)}==([^\s;\\]+)", RUNTIME.read_text(encoding="utf-8"), re.M, ) assert match, f"{name} is not pinned in requirements/runtime.txt" return match.group(1) def _version_tuple(version: str) -> tuple: """Compare dotted versions without packaging (test-only helper).""" return tuple(int(part) for part in version.split(".")) def _project_specifier(name: str) -> str: """Return the full dependency clause for a pip name in pyproject.toml.""" data = tomllib.loads(PYPROJECT.read_text(encoding="utf-8")) for dep in data["project"]["dependencies"]: pip_name = re.match(r"^([A-Za-z0-9._-]+)", dep).group(1).lower().replace("_", "-") if pip_name == name: return dep raise AssertionError(f"{name} is missing from pyproject.toml dependencies") def _template_version(name: str) -> str: """Return the template version for a template directory name.""" return _template_vars(name)["version"] def _void_pins() -> dict: """Parse packaging/void/tool_checksums.txt (name -> kind, value, source).""" pins = {} for line in PINS.read_text(encoding="utf-8").splitlines(): if not line.strip() or line.lstrip().startswith("#"): continue entry = line.split(None, 3) assert len(entry) == 4, f"unparseable pin line: {line!r}" pins[entry[0]] = (entry[1], entry[2], entry[3].strip()) return pins def test_templates_live_in_void_packages_layout() -> None: """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" def test_pkgname_matches_template_directory() -> None: """Void requires pkgname to match srcpkgs/.""" for name in TEMPLATES: assert _template_vars(name)["pkgname"] == name, name def test_mandatory_void_variables_are_present() -> None: """The Manual's mandatory variables plus fetch inputs must be set.""" for name in TEMPLATES: variables = _template_vars(name) missing = [key for key in MANDATORY_VARS if not variables.get(key)] assert not missing, f"{name} is missing: {missing}" assert "python3-setuptools" in variables["hostmakedepends"].split(), name pyaudio = _template_vars("python3-PyAudio") assert "portaudio-devel" in pyaudio.get("makedepends", "").split() def test_template_versions_match_lock_and_runtime_export() -> None: """Templates track the locked sdist versions, not newest upstream.""" for name in TEMPLATES: version, _ = _locked_sdist(LOCK_NAMES[name]) assert _template_version(name) == version, name assert _exported_version(LOCK_NAMES[name]) == version, name def test_template_checksums_match_lock_sdist() -> None: """The template checksum is the hash uv already recorded for the sdist.""" for name in TEMPLATES: _, digest = _locked_sdist(LOCK_NAMES[name]) assert _template_vars(name)["checksum"] == digest, name @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.""" 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 else: assert template >= _version_tuple(floor), specifier @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(bound) for operator, bound in re.findall(r"(>=|==|~=|>)\s*([\d][\d.]*)", specifier) if operator in (">=", "==", "~=", ">") ] 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: """Dependency verification stays on; distfiles stay checksum-pinned.""" for name in TEMPLATES: variables = _template_vars(name) for key in FORBIDDEN_VARS: assert key not in variables, f"{name} sets {key}" assert re.fullmatch(r"[0-9a-f]{64}", variables["checksum"]), name assert variables["distfiles"], name for moving in ("/master", "/main", "/continuous"): assert moving not in variables["distfiles"], f"{name}: {moving}" def test_pynput_template_covers_linux_runtime_deps() -> None: """Every linux-marked install_requires of pynput resolves to a dep.""" depends = set(_template_vars("python3-pynput").get("depends", "").split()) missing = [dep for dep in PYNPUT_LINUX_DEPS if dep not in depends] assert not missing, f"python3-pynput depends is missing: {missing}" def test_template_dependencies_exist_in_void_packages() -> None: """Template dependency lists name only verified void-packages names.""" for name in TEMPLATES: variables = _template_vars(name) 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"{name} references unverified packages: {unknown}" def test_void_container_base_image_is_digest_pinned() -> None: """The build container is pinned by index digest, like the other gates.""" pins = _void_pins() assert "base-image" in pins, "base-image pin is missing" kind, value, source = pins["base-image"] assert kind == "docker", f"base-image kind is {kind}" assert "@sha256:" in value, "a tag is not a pin" assert source, "the source tag the digest came from must be recorded" def test_void_template_metadata_shape() -> None: """Maintainer, license, and description follow the Manual's shapes.""" for name in TEMPLATES: variables = _template_vars(name) assert re.fullmatch( r".+ <[^@<>\s]+@[^@<>\s]+>", variables["maintainer"] ), f"{name}: bad maintainer" assert "noreply" not in variables["maintainer"], name assert variables["license"] == EXPECTED_LICENSES[name], name assert len(variables["short_desc"]) <= 72, name assert re.fullmatch(r"[0-9][A-Za-z0-9.]*", variables["version"]), name assert variables["revision"].isdigit(), name