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
+8
View File
@@ -57,6 +57,14 @@ is not `repository-key.plist`. The private key is never committed; only the
public `.plist` is. Key generation and the check that a private key matches the
committed public key are in `packaging/void/README.md`.
The carried Void templates in `packaging/void/srcpkgs/` track the project's own
pins. `python3-PyAudio` and `python3-pysocks` replace stock Void packages below
the floors in `pyproject.toml`, and `voculinux` depends on each with a `>=`
constraint. When a floor or a carried version changes, change the template and
its constraint in the same pull request: the offline tests in
`tests/test_void_small_deps.py` and `tests/test_void_app_template.py` fail
until they agree. The reasoning is in `packaging/void/README.md`, "Version floors".
## Quick Release Checklist
Use this checklist for every release:
+51 -9
View File
@@ -21,6 +21,7 @@ packaging/void/
├── python3-pywhispercpp/ ticket #26: whisper.cpp Python binding
├── python3-pynput/ ticket #25: the input library Void does not ship
├── python3-PyAudio/ ticket #25: the audio binding, updated past Void's copy
├── python3-pysocks/ spec #16 gap: the SOCKS module, updated past Void's copy
└── voculinux ticket #27: the application
```
@@ -33,9 +34,45 @@ packaging/void/
| Template | Ticket | Notes |
|---|---|---|
| `python3-pywhispercpp` | #26 | Vulkan backend on by default; see below |
| `python3-pynput`, `python3-PyAudio` | #25 | Dependencies Void does not ship at the needed version |
| `python3-pynput` | #25 | A dependency Void does not ship |
| `python3-PyAudio`, `python3-pysocks` | spec #16 gap | Void's copies are below the project floors; see [Version floors](#version-floors) |
| `voculinux` | #27 | The app, from the canonical release sdist; see below |
## Version floors
`pyproject.toml` declares floors that Void's own packages do not meet. On
2026-11-06 the stock packages on void-packages master were:
| Package | Stock Void | Project floor | Carried template | `voculinux` depends |
|---|---|---|---|---|
| `python3-PyAudio` | 0.2.12_5 | `pyaudio>=0.2.13` | 0.2.14_1 | `python3-PyAudio>=0.2.14_1` |
| `python3-pysocks` | 1.7.0_8 | `PySocks>=1.7.1,<2` | 1.7.1_1 | `python3-pysocks>=1.7.1_1` |
The gap is closed in the personal repository, not hidden. The carried
templates build the versions `uv.lock` pins, and `voculinux` names each one
with a `>=` constraint in the xbps `name>=version_revision` form (the xbps-src
manual lists `<`, `>`, `<=` and `>=`). A bare name would be satisfied by the
stock package a user already has, so xbps would leave it in place and the app
would run below its floor. With the constraint, xbps takes the carried
template from the signed repository instead. Dependency verification stays on.
The install smoke checks the upgrade, not just a clean install: it installs
the stock packages from Void first, then `voculinux`, and fails unless each
package reads back as its carried pkgver.
`tests/test_void_app_template.py` requires every constraint to name its
template's current version and revision, so a template bump without a
`depends` bump fails offline. `tests/test_void_small_deps.py` requires the
stock version below the floor and the carried version at or above it.
Known limit: xbps has one comparator per dependency, so PySocks' `<2` cannot
be written. The carried 1.7.1 satisfies it. A future Void `python3-pysocks`
2.x would also satisfy `>=1.7.1_1`, so it is not excluded by the dependency.
When a project floor rises, or Void's stock package reaches the floor, change
the carried template and its constraint in the same pull request. The
stock-below-floor test fails once Void catches up, which is the signal to
retire the carried template deliberately.
## python3-pywhispercpp
Version `1.5.0`, pinned to the same version `install.sh`
@@ -70,9 +107,11 @@ upstream. Read [Version mapping](#version-mapping) and [Checksum](#checksum)
before changing `version`, `_tag` or `checksum`.
- **Runtime deps** are Void's own interpreter, GTK and desktop bindings,
the packaged Python libraries, the three templates from tickets #25
and #26 (`python3-pynput`, `python3-PyAudio`, `python3-pywhispercpp`),
and the helper tools the app shells out to: PipeWire (capture), `wtype`
the packaged Python libraries, the carried templates (`python3-pynput`,
`python3-PyAudio`, `python3-pysocks` and `python3-pywhispercpp`; the audio
binding and SOCKS module are version-constrained, see
[Version floors](#version-floors)), and the helper tools the app shells
out to: PipeWire (capture), `wtype`
(direct typing on niri), `wl-clipboard`, `xdotool` and
`pulseaudio-utils`. `ibus` is not a dependency: it cannot inject on
niri. Every name was verified to exist in current void-packages; the
@@ -156,11 +195,14 @@ host Void install and nothing is published. Stages, in order, each fails the run
with `xlint`, built with `xbps-src`, the sdist resolved by `xbps-src show`;
4. `sign`: the packages indexed and signed, with no network;
5. `install`: in a clean image with no key, an unsigned install is refused, the
signed repository is trusted, `voculinux` installs, `voculinux --version`
shows the version, the payload checks pass (desktop entry, icon, marker, no
models, no pre-rename names, the Vulkan backend, no host-native CPU tuning),
and the imports load (`pynput` through its dummy backend, `evdev`, `gi` with
Gtk 3, `pywhispercpp.model`, `pyaudio`, and the app's own modules, which keep their retained internal name).
signed repository is trusted, Void's stock `python3-PyAudio` and
`python3-pysocks` are installed, then `voculinux` installs and upgrades both
to the carried versions (see [Version floors](#version-floors)),
`voculinux --version` shows the version, the payload checks pass (desktop
entry, icon, marker, no models, no pre-rename names, the Vulkan backend, no
host-native CPU tuning), and the imports load (`pynput` through its dummy
backend, `evdev`, `gi` with Gtk 3, `pywhispercpp.model`, `pyaudio`, `socks`,
and the app's own modules, which keep their retained internal name).
`tests/test_void_smoke_gate.py` asserts offline that the gate stays wired to
every `srcpkgs` template.
+15 -2
View File
@@ -137,12 +137,24 @@ phase_install() {
xbps-query voculinux >/dev/null 2>&1 && fail "voculinux was installed by the untrusted attempt"
echo "refused without the key, as required (rc=$REFUSAL_RC)"
echo "== trust the staged key, then install voculinux from the signed repository"
echo "== trust the staged key"
cp "$XBPS_STAGE"/keys/*.plist /var/db/xbps/keys/
echo "== stock Void packages below the project floors, as a user who has them already"
xbps-install -y --repository="$OFFICIAL" -S python3-PyAudio python3-pysocks >/dev/null
echo "== install voculinux from the signed repository; the floors must upgrade the stock packages"
xbps-install -y --repository="$XBPS_STAGE/repo" --repository="$OFFICIAL" -S voculinux >/dev/null
xbps-query voculinux | grep -qx "pkgver: voculinux-${XBPS_VERSION}_1" ||
fail "installed package is not voculinux-${XBPS_VERSION}_1"
echo "installed: voculinux-${XBPS_VERSION}_1"
for PKG in python3-PyAudio python3-pysocks; do
PKGFILE=$(ls "$PKG_STAGE"/"$PKG"-[0-9]*.x86_64.xbps | head -n1)
PKGVER=$(basename "$PKGFILE" .x86_64.xbps)
xbps-query "$PKG" | grep -qx "pkgver: $PKGVER" ||
fail "$PKG is not $PKGVER after voculinux: the stock package was not upgraded"
echo "upgraded to the carried template: $PKGVER"
done
echo "== the renamed binary reports its version"
VERSION_OUT=$(voculinux --version 2>&1) || fail "voculinux --version exited non-zero"
@@ -155,7 +167,7 @@ phase_install() {
case "$PKG" in
voculinux) payload_voculinux "$PKGFILE" ;;
python3-pywhispercpp) payload_pywhispercpp "$PKGFILE" ;;
python3-pynput | python3-PyAudio) ;; # checked by import below
python3-pynput | python3-PyAudio | python3-pysocks) ;; # checked by import below
*) fail "no smoke recipe for package $PKG; add one alongside its template" ;;
esac
done
@@ -168,6 +180,7 @@ phase_install() {
python3 -c "import gi; gi.require_version('Gtk', '3.0'); from gi.repository import Gtk; print('gi ok: Gtk', Gtk.get_major_version(), Gtk.get_minor_version())"
python3 -c "from pywhispercpp.model import Model; print('pywhispercpp.model ok')"
python3 -c "import pyaudio; print('pyaudio ok')"
python3 -c "import socks; print('pysocks ok')"
python3 -c "import vocalinux.main, vocalinux.speech_recognition.recognition_manager; print('vocalinux imports ok')"
echo "INSTALL SMOKE OK: voculinux $VERSION installed from the signed repository"
}
+1 -1
View File
@@ -20,6 +20,6 @@ HERE=$(cd -- "$HERE" && pwd)
# Dependency order: the app last. tests/test_void_smoke_gate.py fails if this
# list drifts from packaging/void/srcpkgs.
# shellcheck disable=SC2034 # read as data by build-release.sh, which parses this line
PKGS="python3-PyAudio python3-pynput python3-pywhispercpp voculinux"
PKGS="python3-PyAudio python3-pynput python3-pysocks python3-pywhispercpp voculinux"
exec "$HERE/build-release.sh" smoke
@@ -0,0 +1,20 @@
# Template file for 'python3-pysocks'
# Carried for the personal repository: Void's python3-pysocks is 1.7.0, below
# the project floor PySocks>=1.7.1 (pyproject.toml). The version and checksum
# are the PyPI sdist that uv.lock pins. See packaging/void/README.md.
pkgname=python3-pysocks
version=1.7.1
revision=1
build_style=python3-module
hostmakedepends="python3-setuptools"
depends="python3"
short_desc="Python3 SOCKS client module"
maintainer="Orphaned <orphan@voidlinux.org>"
license="BSD-3-Clause"
homepage="https://github.com/Anorov/PySocks"
distfiles="${PYPI_SITE}/P/PySocks/PySocks-${version}.tar.gz"
checksum=3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0
post_install() {
vlicense LICENSE
}
+6 -1
View File
@@ -15,7 +15,12 @@ makedepends="python3-devel"
# direct typing on niri, wl-clipboard (wl-copy, wl-paste) for clipboard
# paths, xdotool for X11 and the XWayland fallback, pulseaudio-utils for
# pactl source listing. ibus is not a dependency: it is excluded on niri.
depends="python3 python3-gobject python3-cairo gtk+3 libayatana-appindicator gobject-introspection python3-numpy python3-requests python3-pysocks python3-psutil python3-evdev python3-xlib python3-tqdm python3-platformdirs python3-PyAudio python3-pynput python3-pywhispercpp pipewire wtype wl-clipboard xdotool pulseaudio-utils"
# python3-PyAudio and python3-pysocks are version-constrained. Void's stock
# packages (0.2.12_5, 1.7.0_8) sit below the project floors, and a bare name is
# already satisfied by a stock package a user has installed, so it would never
# be upgraded to the carried template. Each >= names the carried template's
# version and revision; tests/test_void_app_template.py keeps them in step.
depends="python3 python3-gobject python3-cairo gtk+3 libayatana-appindicator gobject-introspection python3-numpy python3-requests python3-pysocks>=1.7.1_1 python3-psutil python3-evdev python3-xlib python3-tqdm python3-platformdirs python3-PyAudio>=0.2.14_1 python3-pynput python3-pywhispercpp pipewire wtype wl-clipboard xdotool pulseaudio-utils"
short_desc="Free, offline voice dictation for Linux"
maintainer="Xavier Karma <xavier@bongbetic.com>"
license="AGPL-3.0-only"
+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: