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
243 lines
11 KiB
Python
243 lines
11 KiB
Python
"""Offline policy tests for the Void template of the whisper.cpp binding (#26).
|
|
|
|
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
|
|
smoke gate on top; everything asserted here runs offline from sources
|
|
already in the repo.
|
|
|
|
Independent sources of truth: ``install.sh`` pins the binding version
|
|
(``PYWHISPERCPP_VERSION``), ``uv.lock`` pins the sdist URL and sha256, and
|
|
the sdist's own ``setup.py``/``PKG-INFO`` name the build and runtime
|
|
requirements. The template must agree with all three.
|
|
"""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
VOID_DIR = REPO_ROOT / "packaging" / "void"
|
|
TEMPLATE = VOID_DIR / "srcpkgs" / "python3-pywhispercpp" / "template"
|
|
PINS = VOID_DIR / "tool_checksums.txt"
|
|
BUILD_SH = VOID_DIR / "build-release.sh"
|
|
INSTALL_SH = REPO_ROOT / "install.sh"
|
|
UV_LOCK = REPO_ROOT / "uv.lock"
|
|
|
|
#: ggml SIMD options pinned off for the plain x86-64 baseline (#957).
|
|
#: GGML_NATIVE=OFF alone is not portable: while ggml's native default is on,
|
|
#: every GGML_* SIMD option stays on and still compiles -mavx2 -mfma.
|
|
#: Mirrors PORTABLE_GGML_OPTIONS in tests/test_flatpak_packaging.py and the
|
|
#: CMAKE_ARGS line in packaging/flatpak/python3-dependencies.yaml.
|
|
PORTABLE_GGML_OPTIONS = (
|
|
"GGML_NATIVE",
|
|
"GGML_SSE42",
|
|
"GGML_AVX",
|
|
"GGML_AVX2",
|
|
"GGML_BMI2",
|
|
"GGML_FMA",
|
|
"GGML_F16C",
|
|
)
|
|
|
|
#: install_requires of pywhispercpp's setup.py, mapped to the Void binary
|
|
#: package names. Each name was verified to exist as srcpkgs/<name>/template
|
|
#: in current void-packages (2026-11-05); the container build re-verifies via
|
|
#: xbps-query, so a removal upstream fails there rather than silently here.
|
|
VOID_RUNTIME_DEPS = (
|
|
"python3-numpy",
|
|
"python3-requests",
|
|
"python3-tqdm",
|
|
"python3-platformdirs",
|
|
)
|
|
|
|
|
|
def _template() -> str:
|
|
assert TEMPLATE.is_file(), f"missing Void template: {TEMPLATE}"
|
|
return TEMPLATE.read_text(encoding="utf-8")
|
|
|
|
|
|
def _pinned_version() -> str:
|
|
text = INSTALL_SH.read_text(encoding="utf-8")
|
|
match = re.search(r'^PYWHISPERCPP_VERSION="([^"]+)"', text, re.M)
|
|
assert match, "install.sh no longer pins PYWHISPERCPP_VERSION"
|
|
return match.group(1)
|
|
|
|
|
|
def _sdist_url_and_hash() -> tuple[str, str]:
|
|
text = UV_LOCK.read_text(encoding="utf-8")
|
|
match = re.search(
|
|
r'sdist = \{ url = "(https://files\.pythonhosted\.org/[^"]*?'
|
|
r"/pywhispercpp-([0-9.]+)\.tar\.gz)\","
|
|
r' hash = "sha256:([0-9a-f]{64})"',
|
|
text,
|
|
)
|
|
assert match, "uv.lock no longer pins the pywhispercpp sdist URL and hash"
|
|
return match.group(1), match.group(3)
|
|
|
|
|
|
def test_template_names_the_package_after_its_directory() -> None:
|
|
"""pkgname must match srcpkgs/<pkgname>, per the Void manual."""
|
|
text = _template()
|
|
match = re.search(r"^pkgname=(\S+)", text, re.M)
|
|
assert match, "template sets no pkgname"
|
|
assert match.group(1) == TEMPLATE.parent.name == "python3-pywhispercpp"
|
|
|
|
|
|
def test_template_carries_the_mandatory_void_variables() -> None:
|
|
"""Mandatory vars: version, revision, short_desc, maintainer, license."""
|
|
text = _template()
|
|
assert re.search(r"^version=[0-9][0-9.]*$", text, re.M), "unpinned or missing version"
|
|
assert re.search(r"^revision=1$", text, re.M), "new templates start at revision 1"
|
|
match = re.search(r'^short_desc="([^"]+)"', text, re.M)
|
|
assert match, "missing short_desc"
|
|
assert len(match.group(1)) <= 72, f"short_desc exceeds 72 chars: {match.group(1)!r}"
|
|
assert re.search(
|
|
r'^maintainer="?[^"]*<\S+@\S+>"?$', text, re.M
|
|
), "maintainer is not 'name <email>'"
|
|
assert re.search(r'^license="?MIT"?$', text, re.M), "pywhispercpp is MIT licensed"
|
|
assert re.search(r'^homepage="?https://github\.com/absadiki/pywhispercpp"?$', text, re.M)
|
|
|
|
|
|
def test_template_version_matches_the_installer_and_lockfile_pins() -> None:
|
|
"""The template builds the pinned binding, not newest upstream."""
|
|
text = _template()
|
|
pinned = _pinned_version()
|
|
match = re.search(r"^version=(\S+)", text, re.M)
|
|
assert match and match.group(1) == pinned, (
|
|
f"template builds {match.group(1) if match else 'nothing'}, " f"install.sh pins {pinned}"
|
|
)
|
|
|
|
|
|
def test_distfiles_and_checksum_match_the_locked_sdist() -> None:
|
|
"""One PyPI source distribution, checksum-verified against uv.lock."""
|
|
text = _template()
|
|
url, sha = _sdist_url_and_hash()
|
|
assert (
|
|
"${PYPI_SITE}/p/pywhispercpp/pywhispercpp-${version}.tar.gz" in text
|
|
), "distfiles must use the ${PYPI_SITE} mirror helper with ${version}"
|
|
match = re.search(r"^checksum=([0-9a-f]{64})$", text, re.M)
|
|
assert match, "checksum must be a bare sha256 hex digest"
|
|
assert match.group(1) == sha, "template checksum is behind uv.lock's sdist hash"
|
|
assert url.endswith(f"pywhispercpp-{_pinned_version()}.tar.gz")
|
|
|
|
|
|
def test_build_style_is_python3_module() -> None:
|
|
"""setup.py exists, so the module style applies (per the Void manual)."""
|
|
assert re.search(r"^build_style=python3-module$", _template(), re.M)
|
|
|
|
|
|
def test_portable_cpu_flags_pin_every_ggml_simd_option_off() -> None:
|
|
"""No host-native tuning: the plain x86-64 baseline, like the Flatpak."""
|
|
text = _template()
|
|
for flag in PORTABLE_GGML_OPTIONS:
|
|
assert f"-D{flag}=OFF" in text, (
|
|
f"template does not pin {flag} off; the package inherits the "
|
|
"builder's instruction set and SIGILLs on older CPUs (#938, #957)"
|
|
)
|
|
|
|
|
|
def _code_lines() -> str:
|
|
"""Template with comment lines stripped; flags live in code, not prose."""
|
|
return "\n".join(line for line in _template().splitlines() if not line.lstrip().startswith("#"))
|
|
|
|
|
|
def test_no_native_tuning_or_cuda_anywhere() -> None:
|
|
"""Void has no CUDA toolkit package; portable means portable."""
|
|
text = _code_lines()
|
|
assert "-march=native" not in text and "-mcpu=native" not in text
|
|
assert "GGML_NATIVE=ON" not in text
|
|
assert "cuda" not in text.lower(), "no CUDA build: Void ships no CUDA toolkit"
|
|
|
|
|
|
def test_vulkan_is_a_default_on_build_option() -> None:
|
|
"""Source builders rebuild CPU-only with -o ~vulkan."""
|
|
text = _template()
|
|
match = re.search(r"^build_options=(.*)$", text, re.M)
|
|
assert match and "vulkan" in match.group(1).replace('"', "").split()
|
|
match = re.search(r"^build_options_default=(.*)$", text, re.M)
|
|
assert match and "vulkan" in match.group(1).replace('"', "").split(), "Vulkan is on by default"
|
|
# The common options description already defines desc_option_vulkan;
|
|
# a local redefinition would drift from it.
|
|
assert "desc_option_vulkan" not in text
|
|
assert "GGML_VULKAN=1" in text, "the Vulkan backend is enabled via GGML_VULKAN=1"
|
|
assert "build_option_vulkan" in text, "GGML_VULKAN must be gated on the option"
|
|
|
|
|
|
def test_vulkan_deps_are_conditional_and_runtime_names_exist() -> None:
|
|
"""Vulkan-only deps vanish from CPU-only builds; runtime names are real."""
|
|
text = _template()
|
|
assert "vopt_if vulkan" in text, "conditional Vulkan deps use vopt_if"
|
|
assert "vulkan-loader" in text, "libvulkan comes from vulkan-loader"
|
|
assert "Vulkan-Headers" in text or "vulkan-loader-devel" in text
|
|
assert "shaderc" in text, "ggml-vulkan requires glslc at build time"
|
|
depends = re.search(r"^depends=(.*)$", text, re.M)
|
|
assert depends, "template declares no runtime depends"
|
|
for dep in VOID_RUNTIME_DEPS:
|
|
assert dep in depends.group(1), f"{dep} missing from depends"
|
|
|
|
|
|
def test_build_time_deps_cover_setup_py() -> None:
|
|
"""setup.py imports bdist_wheel at top level and shells out to cmake."""
|
|
text = _template()
|
|
host = re.search(r"^hostmakedepends=(.*)$", text, re.M)
|
|
assert host, "template declares no hostmakedepends"
|
|
for dep in ("cmake", "pkg-config", "python3-setuptools", "python3-wheel"):
|
|
assert dep in host.group(1), f"{dep} missing from hostmakedepends"
|
|
make = re.search(r"^makedepends=(.*)$", text, re.M)
|
|
assert make and "python3-devel" in make.group(1), "compiled extension needs python3-devel"
|
|
|
|
|
|
def test_version_is_injected_for_the_offline_build() -> None:
|
|
"""The sdist carries no version.txt; setup.py reads PYWHISPERCPP_VERSION."""
|
|
assert "PYWHISPERCPP_VERSION" in _template()
|
|
|
|
|
|
def test_build_fetches_nothing_beyond_the_sdist() -> None:
|
|
"""Only declared distfiles may come from the network."""
|
|
text = _template()
|
|
assert text.count("https://") == 1, "homepage is the only URL a template may name"
|
|
assert "FetchContent" not in text and "ExternalProject" not in text
|
|
assert "make_check=no" in text, "the sdist ships no tests to check"
|
|
|
|
|
|
def test_template_style_is_lint_clean() -> None:
|
|
"""Trailing whitespace and missing final newline fail xlint."""
|
|
raw = TEMPLATE.read_bytes().decode("utf-8")
|
|
for lineno, line in enumerate(raw.splitlines(), 1):
|
|
assert line == line.rstrip(), f"trailing whitespace on line {lineno}"
|
|
assert "\t" not in line or line.startswith("\t"), f"stray tab on line {lineno}"
|
|
assert raw.endswith("\n"), "template must end with a newline"
|
|
|
|
|
|
def test_tool_checksums_pin_the_void_glibc_image() -> None:
|
|
"""The container gate builds in one pinned image, like native/appimage."""
|
|
assert PINS.is_file(), f"missing pins file: {PINS}"
|
|
pins = {}
|
|
for line in PINS.read_text(encoding="utf-8").splitlines():
|
|
if not line.strip() or line.lstrip().startswith("#"):
|
|
continue
|
|
name, kind, value, _source = line.split(None, 3)
|
|
pins[name] = (kind, value)
|
|
assert "base-image" in pins, "no pinned base image"
|
|
kind, value = pins["base-image"]
|
|
assert kind == "docker", "base-image pin kind matches native/appimage convention"
|
|
assert value.startswith(
|
|
"ghcr.io/void-linux/void-glibc-full@sha256:"
|
|
), f"base image must be the pinned Void glibc image, got {value}"
|
|
assert len(value.rsplit(":", 1)[1]) == 64, "image digest must be a full sha256"
|
|
|
|
|
|
def test_build_script_uses_podman_and_the_pinned_image() -> None:
|
|
"""Container work uses podman, never Docker; the image comes from the pins."""
|
|
assert BUILD_SH.is_file(), f"missing build script: {BUILD_SH}"
|
|
text = BUILD_SH.read_text(encoding="utf-8")
|
|
assert "podman" in text, "the build harness must use podman"
|
|
scrubbed = text.lower().replace("never docker", "")
|
|
assert "docker" not in scrubbed, "never Docker"
|
|
assert "tool_checksums.txt" in text, "the image digest comes from the pins file"
|
|
# The container flow lives in a mounted file, not an inline sh -c string:
|
|
# single quotes inside sh -c silently regroup the command.
|
|
name = "container-release.sh"
|
|
assert (VOID_DIR / name).is_file(), f"missing container script: {name}"
|
|
assert name in text, f"{BUILD_SH.name} must mount {name}"
|