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
164 lines
7.2 KiB
Python
164 lines
7.2 KiB
Python
"""Offline policy guards for the Void package smoke gate (ticket #28).
|
|
|
|
Spec #16 gives the packaged artefact the same treatment as the other
|
|
distribution formats: one documented command runs lint, build, install
|
|
and version/import smoke for every Void template from a clean checkout,
|
|
using podman and the pinned container, with no host Void install.
|
|
|
|
Seams under test (public interfaces, not internals):
|
|
|
|
- ``packaging/void/smoke.sh`` — the gate command. It runs the same release
|
|
path as a tag (``build-release.sh smoke``), so a passing gate is a release
|
|
that would build.
|
|
- The ``void-smoke`` just recipe — how release work and CI call the
|
|
gate, same shape as the other packaging gates.
|
|
- ``packaging/void/README.md`` — the documented one-command entry.
|
|
|
|
Independent sources of truth: ``packaging/void/srcpkgs/*/template``
|
|
owns the template set (a renamed or added template must not silently
|
|
drop out), ``packaging/void/tool_checksums.txt`` owns the pins, and
|
|
``packaging/void/container-release.sh`` owns the install smoke each
|
|
package must pass. The gate must agree with all of them; the container
|
|
run itself is the proof, these tests are the wiring guard.
|
|
"""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
VOID = REPO_ROOT / "packaging" / "void"
|
|
SRC = VOID / "srcpkgs"
|
|
SMOKE_SH = VOID / "smoke.sh"
|
|
RELEASE_PATH = VOID / "build-release.sh"
|
|
CONTAINER_RELEASE = VOID / "container-release.sh"
|
|
PINS = VOID / "tool_checksums.txt"
|
|
JUSTFILE = REPO_ROOT / "justfile"
|
|
README = VOID / "README.md"
|
|
|
|
|
|
def _srcpkg_templates() -> list:
|
|
"""Every template directory under srcpkgs, in dependency build order."""
|
|
return sorted(p.name for p in SRC.iterdir() if (p / "template").is_file())
|
|
|
|
|
|
def _smoke_text() -> str:
|
|
assert SMOKE_SH.is_file(), f"missing Void smoke gate: {SMOKE_SH}"
|
|
return SMOKE_SH.read_text(encoding="utf-8")
|
|
|
|
|
|
def _gate_packages() -> list:
|
|
"""The ordered package list the gate builds, parsed from smoke.sh."""
|
|
match = re.search(r'^PKGS="([^"]+)"', _smoke_text(), re.M)
|
|
assert match, "smoke.sh must declare its ordered PKGS list"
|
|
return match.group(1).split()
|
|
|
|
|
|
def test_gate_builds_every_template_and_nothing_else() -> None:
|
|
"""A renamed or added template cannot silently drop out of the smoke."""
|
|
assert _gate_packages() and set(_gate_packages()) == set(
|
|
_srcpkg_templates()
|
|
), f"gate covers {_gate_packages()}, srcpkgs holds {_srcpkg_templates()}"
|
|
|
|
|
|
def test_gate_builds_the_app_last() -> None:
|
|
"""voculinux resolves the other templates from the local repository."""
|
|
assert (
|
|
_gate_packages()[-1] == "voculinux"
|
|
), f"the app must build last, gate order is {_gate_packages()}"
|
|
|
|
|
|
def test_gate_delegates_to_the_release_path() -> None:
|
|
"""One release path; the gate only selects the smoke mode, and its exit status is the path's."""
|
|
text = _smoke_text()
|
|
assert 'exec "$HERE/build-release.sh" smoke' in text, "smoke.sh must exec the release path"
|
|
assert "build-release.sh" in RELEASE_PATH.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_gate_stays_podman_only_with_pinned_inputs() -> None:
|
|
"""Podman, never Docker; pins live in tool_checksums.txt, not the gate."""
|
|
text = _smoke_text()
|
|
assert "podman" in text, "the smoke gate must use podman"
|
|
assert "docker" not in text.lower().replace("never docker", "")
|
|
assert "ghcr.io" not in text, "the base image pin lives in tool_checksums.txt"
|
|
assert PINS.is_file(), f"missing pins file: {PINS}"
|
|
|
|
|
|
def _case_branch(label: str) -> str:
|
|
"""Shell lines of one case branch in the install phase of container-release.sh."""
|
|
text = CONTAINER_RELEASE.read_text(encoding="utf-8")
|
|
start = text.index(f"\n\t\t{label})")
|
|
end = text.index(";;", start)
|
|
return text[start:end]
|
|
|
|
|
|
def test_the_ggml_scan_belongs_to_the_whisper_binding_only() -> None:
|
|
"""Only the whisper binding ships libggml; the payload check is its own."""
|
|
assert "payload_pywhispercpp" in _case_branch("python3-pywhispercpp")
|
|
assert "payload_voculinux" not in _case_branch("python3-pywhispercpp")
|
|
text = CONTAINER_RELEASE.read_text(encoding="utf-8")
|
|
assert "libggml-cpu" in text and "libggml-vulkan" in text
|
|
|
|
|
|
def test_the_leaf_packages_are_imported_headless() -> None:
|
|
"""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:
|
|
"""A future template falls into *) and must fail loudly, never pass mute."""
|
|
text = CONTAINER_RELEASE.read_text(encoding="utf-8")
|
|
assert "no smoke recipe for package" in _case_branch("*") or (
|
|
"no smoke recipe for package" in text
|
|
)
|
|
|
|
|
|
def test_just_recipe_calls_the_gate_like_other_packaging_gates() -> None:
|
|
"""`just void-smoke` runs the gate script with no shared-output step."""
|
|
recipe = re.search(
|
|
r"^void-smoke:([^\n]*)\n((?:[ \t].*\n|\n)+)",
|
|
JUSTFILE.read_text(encoding="utf-8"),
|
|
re.MULTILINE,
|
|
)
|
|
assert recipe, "justfile must define a void-smoke recipe"
|
|
assert not recipe.group(1).strip(), "void-smoke must not take a shared-output prerequisite"
|
|
assert "packaging/void/smoke.sh" in recipe.group(
|
|
2
|
|
), "void-smoke must run packaging/void/smoke.sh"
|
|
|
|
|
|
def test_readme_documents_the_one_command_gate() -> None:
|
|
"""AC1: one documented command from a clean checkout, no host Void."""
|
|
text = README.read_text(encoding="utf-8")
|
|
assert "void-smoke" in text or "smoke.sh" in text, "README must document the smoke gate command"
|
|
assert "podman" in text.lower(), "README must state the gate uses podman"
|