feat(void): add xbps template for python3-pywhispercpp

Spec #16 ticket #26: Void template for the whisper.cpp Python binding,
built from the pinned 1.5.0 sdist with the Vulkan backend (default-on
option), shared libraries, and portable CPU flags. Includes the podman
build/smoke harness, pinned base-image pins, and offline policy tests.

Refs: #26
This commit is contained in:
soubarnak
2026-11-06 00:04:47 +05:30
parent 0a4d637f5f
commit 1d8b07727e
7 changed files with 533 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
# Void packaging
Void Linux (glibc, x86_64) templates in void-packages layout, built with the
standard Void source tooling. Submission to upstream void-packages is out of
scope for spec #16; these templates serve the signed personal xbps
repository first.
## Layout
```
packaging/void/
├── README.md this file
├── build.sh podman harness: lint + xbps-src build + import smoke
├── tool_checksums.txt pinned base image and void-packages commit
└── srcpkgs/
├── python3-pywhispercpp/ ticket #26: whisper.cpp Python binding
├── ... ticket #25: small Python dependencies
└── voculinux ticket #27: the application
```
`srcpkgs/` mirrors void-packages so the tree overlays cleanly:
`cp -r packaging/void/srcpkgs/. /path/to/void-packages/srcpkgs/`
(`build.sh` does this inside the container.)
## Templates
| Template | Ticket | Notes |
|---|---|---|
| `python3-pywhispercpp` | #26 | Vulkan backend on by default; see below |
## python3-pywhispercpp
Version `1.5.0`, pinned to the same version `install.sh`
(`PYWHISPERCPP_VERSION`) and `uv.lock` pin — never newest upstream.
The sdist checksum in the template must equal the `sha256` uv.lock records
for the sdist; `tests/test_void_pywhispercpp_template.py` fails otherwise.
- **Vulkan** is a build option, on by default. Rebuild CPU-only with
`build.sh python3-pywhispercpp -- -o ~vulkan pkg`. There is no CUDA
build: Void ships no CUDA toolkit package. Rebuilding locally with a
hand-installed toolkit (`GGML_CUDA=1`) is a documented, unsupported
escape hatch.
- **Portable CPU flags**: every ggml SIMD option is pinned off
(`-DGGML_NATIVE=OFF -DGGML_SSE42=OFF ...`), the plain x86-64 baseline
already used by the Flatpak (`#938`, `#957`). The offline test asserts
each pin in the template (the build configuration), and the harness
scans the shipped `libggml-cpu` (`.GCC.command.line`, `.comment`) for
recorded `-march=native`/`-mcpu=native`/`-mtune=native` flags and fails
on a match. The recorded-flags scan is a tripwire, not a proof: the
proof is the pinned configuration plus `setup.py`'s documented
`CMAKE_ARGS` passthrough.
- **Shared libraries**: `BUILD_SHARED_LIBS=ON` with an `$ORIGIN` rpath
(as `install.sh` does), so the extension resolves the whisper.cpp
libraries installed beside it with no `LD_LIBRARY_PATH`.
- **Offline build**: the sdist vendors whisper.cpp, pybind11, and the
ggml sources; the build fetches nothing beyond the declared distfiles.
`PYWHISPERCPP_VERSION` is injected because the sdist carries no
`version.txt` (its version otherwise comes from git metadata).
- **Runtime deps** (`python3-numpy`, `python3-requests`, `python3-tqdm`,
`python3-platformdirs`) are the sdist's own `install_requires` and were
verified to exist in current void-packages; the container build
re-verifies via dependency resolution.
## Building
```sh
packaging/void/build.sh python3-pywhispercpp
packaging/void/build.sh python3-pywhispercpp -- -o ~vulkan pkg
```
Podman only, never Docker. The base image digest and the void-packages
commit come from `tool_checksums.txt`; bump them there, never in the
script. Ticket #28 wires this into the package smoke gate.
+51
View File
@@ -0,0 +1,51 @@
#!/bin/sh
# Host-side harness for packaging/void: runs the container flow (see
# container-main.sh and container-build.sh) in the pinned Void glibc
# container via podman, never Docker.
#
# Usage: packaging/void/build.sh <pkgname> [-- <xbps-src args...>]
#
# packaging/void/build.sh python3-pywhispercpp
# packaging/void/build.sh python3-pywhispercpp -- -o ~vulkan pkg
#
# Anything after -- is passed to `xbps-src` before the package name (use it
# for -o ~vulkan CPU-only rebuilds). The package build itself fetches
# nothing from the network beyond the template's declared distfiles:
# xbps-src verifies distfile checksums before unpacking.
set -eu
CDPATH=""
HERE=$(dirname -- "$0")
HERE=$(cd -- "$HERE" && pwd)
REPO_ROOT=$(cd -- "$HERE/../.." && pwd)
PKGNAME=${1:?usage: build.sh "<pkgname>" [-- "<xbps-src args>"]}
shift
if [ "${1:-}" = "--" ]; then
shift
fi
XBPS_ARGS=${*:-pkg}
BASE_IMAGE=$(awk '$1 == "base-image" {print $3}' "$HERE/tool_checksums.txt")
VOID_COMMIT=$(awk '$1 == "void-packages" {print $3}' "$HERE/tool_checksums.txt")
[ -n "$BASE_IMAGE" ] && [ -n "$VOID_COMMIT" ] || {
echo "build.sh: pins missing from tool_checksums.txt" >&2
exit 1
}
case "$XBPS_ARGS" in
*~vulkan*) EXPECT_VULKAN=0 ;;
*) EXPECT_VULKAN=1 ;;
esac
echo "Pulling pinned base image: $BASE_IMAGE"
podman pull "$BASE_IMAGE"
podman run --rm \
-v "$REPO_ROOT/packaging/void/srcpkgs:/overlay:ro" \
-v "$HERE/container-main.sh:/mnt/container-main.sh:ro" \
-v "$HERE/container-build.sh:/mnt/container-build.sh:ro" \
-e "VOID_COMMIT=$VOID_COMMIT" \
-e "PKGNAME=$PKGNAME" \
-e "XBPS_ARGS=$XBPS_ARGS" \
-e "EXPECT_VULKAN=$EXPECT_VULKAN" \
"$BASE_IMAGE" /bin/sh /mnt/container-main.sh
echo "OK: $PKGNAME built and imported cleanly"
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
# Builder-side half of packaging/void/build.sh. Runs inside the pinned Void
# container as the unprivileged builder user (xbps-src refuses root).
#
# Not executed directly: build.sh mounts the repo's srcpkgs next to this
# file, copies it into the container, and runs it after cloning
# void-packages at the pinned commit. PKGNAME and XBPS_ARGS arrive via the
# environment. Keeping this a real file (instead of an inline `sh -c`
# string) means normal quoting, heredocs, and shellcheck apply.
set -eu
export HOME=/home/builder
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
cd /void-packages
echo "== xlint $PKGNAME"
xlint "srcpkgs/$PKGNAME/template"
echo "== show-options $PKGNAME"
./xbps-src show-options "$PKGNAME" || true
echo "== xbps-src $XBPS_ARGS $PKGNAME"
# shellcheck disable=SC2086
./xbps-src $XBPS_ARGS "$PKGNAME"
+73
View File
@@ -0,0 +1,73 @@
#!/bin/sh
# Root-side half of the packaging/void container harness. Runs inside the
# pinned Void container as root: system setup, the void-packages checkout,
# then the builder half (container-build.sh) as the builder user, then the
# install plus import smoke as root again.
#
# Not executed directly: build.sh mounts this file and runs it. PKGNAME,
# XBPS_ARGS, EXPECT_VULKAN and VOID_COMMIT arrive via the environment.
set -eu
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
xbps-install -Suy xbps >/dev/null
xbps-install -y git base-devel xtools binutils zstd >/dev/null
git clone --quiet --depth 1 https://github.com/void-linux/void-packages /void-packages
git -C /void-packages fetch --quiet --depth 1 origin "$VOID_COMMIT"
git -C /void-packages checkout --quiet "$VOID_COMMIT"
cp -r /overlay/. /void-packages/srcpkgs/
id builder >/dev/null 2>&1 || useradd -m builder
cp /mnt/container-build.sh /run-build.sh
chown builder:builder /void-packages /run-build.sh
chmod +x /run-build.sh
su -s /bin/sh builder -c /run-build.sh
echo "== install $PKGNAME from the local repository"
PKGFILE=$(ls /void-packages/hostdir/binpkgs/"$PKGNAME"-*.x86_64.xbps /host/binpkgs/"$PKGNAME"-*.x86_64.xbps 2>/dev/null | head -n1)
[ -n "$PKGFILE" ] || {
echo "built package not found" >&2
exit 1
}
echo "built: $PKGFILE"
xbps-install -Sy >/dev/null
xbps-install -y --repository="$(dirname "$PKGFILE")" "$PKGNAME" >/dev/null
echo "== compiler flag scan of the shipped ggml library"
LIBMEMBER=$(tar -tf "$PKGFILE" 2>/dev/null | grep -E "libggml-cpu\.so" | head -n1)
[ -n "$LIBMEMBER" ] || {
echo "libggml-cpu missing from the built package" >&2
exit 1
}
echo "member: $LIBMEMBER"
mkdir -p /flagscan
tar -xf "$PKGFILE" -C /flagscan "$LIBMEMBER"
LIBSCAN="/flagscan/${LIBMEMBER#./}"
echo "-- .GCC.command.line:"
readelf -p .GCC.command.line "$LIBSCAN" 2>/dev/null || echo "(section absent)"
echo "-- .comment:"
readelf -p .comment "$LIBSCAN" 2>/dev/null || echo "(section absent)"
if readelf -p .GCC.command.line "$LIBSCAN" 2>/dev/null | grep -E "march=native|mcpu=native|mtune=native"; then
echo "host-native CPU tuning in the shipped library" >&2
exit 1
fi
echo "no host-native tuning in the shipped library"
echo "== import smoke $PKGNAME"
python3 -c "from pywhispercpp.model import Model; print('model submodule ok')"
cat > /smoke.py <<'PYEOF'
import glob
import os
import pywhispercpp
pkgdir = os.path.dirname(pywhispercpp.__file__)
sitedir = os.path.dirname(pkgdir)
libs = sorted(os.path.basename(p) for p in glob.glob(sitedir + "/lib*.so*"))
print("shipped libraries:", libs)
assert any(n.startswith("libwhisper") for n in libs), "libwhisper missing"
exts = [n for n in os.listdir(sitedir) if n.startswith("_pywhispercpp") and n.endswith(".so")]
assert exts, "compiled extension missing"
print("extension:", exts)
has_vulkan = any("vulkan" in n for n in libs)
want_vulkan = bool(int(os.environ["EXPECT_VULKAN"]))
assert has_vulkan == want_vulkan, "vulkan backend present=%s, expected=%s" % (has_vulkan, want_vulkan)
print("smoke ok")
PYEOF
EXPECT_VULKAN="$EXPECT_VULKAN" python3 /smoke.py
@@ -0,0 +1,47 @@
# Template file for 'python3-pywhispercpp'
pkgname=python3-pywhispercpp
version=1.5.0
revision=1
build_style=python3-module
hostmakedepends="cmake pkg-config python3-setuptools python3-wheel $(vopt_if vulkan shaderc)"
makedepends="python3-devel $(vopt_if vulkan vulkan-loader-devel)"
depends="python3-numpy python3-requests python3-tqdm python3-platformdirs $(vopt_if vulkan vulkan-loader)"
short_desc="Python bindings for whisper.cpp speech recognition"
maintainer="Orphaned <orphan@voidlinux.org>"
license="MIT"
homepage="https://github.com/absadiki/pywhispercpp"
distfiles="${PYPI_SITE}/p/pywhispercpp/pywhispercpp-${version}.tar.gz"
checksum=5aa54c73ec96617d97aa519dbbf817e44255eddd9bd52ee1243be61028dbd018
# The sdist ships no test suite, so there is nothing to check.
make_check=no
# Package build options
build_options="vulkan"
build_options_default="vulkan"
pre_build() {
# The sdist derives its version from git metadata, which is absent in
# the xbps build root, so inject the pinned version explicitly. This is
# the same version install.sh (PYWHISPERCPP_VERSION) and uv.lock pin.
export PYWHISPERCPP_VERSION=${version}
# Portable x86-64 baseline: GGML_NATIVE is ggml's default and compiles
# libggml-cpu with -march=native, so the package would inherit the
# builder's instruction set and SIGILL on older CPUs (#938).
# GGML_NATIVE=OFF alone is not enough: while the native default is on,
# ggml turns every GGML_* SIMD option on and still compiles -mavx2
# -mfma (#957). Each option is pinned off, mirroring the Flatpak build.
# Shared libraries with an $ORIGIN rpath let the extension resolve the
# whisper.cpp libraries installed beside it (as install.sh does), with
# no LD_LIBRARY_PATH at runtime. (setup.py install never builds a
# wheel, so NO_REPAIR is meaningless here and deliberately unset.)
export CMAKE_ARGS='-DCMAKE_INSTALL_RPATH=$ORIGIN -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON -DBUILD_SHARED_LIBS=ON -DGGML_NATIVE=OFF -DGGML_SSE42=OFF -DGGML_AVX=OFF -DGGML_AVX2=OFF -DGGML_BMI2=OFF -DGGML_FMA=OFF -DGGML_F16C=OFF'
if [ -n "$build_option_vulkan" ]; then
# Vulkan acceleration for every GPU vendor. There is no CUDA
# build: Void ships no CUDA toolkit package.
export GGML_VULKAN=1
fi
}
post_install() {
vlicense LICENSE
}
+22
View File
@@ -0,0 +1,22 @@
# Pinned build inputs for packaging/void/build.sh.
#
# Same contract as packaging/native/tool_checksums.txt: everything the
# package build downloads is pinned and verified here, so two builds of the
# same commit cannot differ.
#
# Columns: name, kind, value, source — parseable by awk.
#
# sha256 the file downloaded from <source> must hash to <value>
# docker build inside image <value>; <source> records the tag it came from
# version pinned commit or tag; <source> says where it was taken from
#
# To bump the base image: pull the new void-glibc-full tag, read the digest
# from `podman inspect` (Digest, not the config hash), and replace <value>.
# The image digest comes from `podman inspect
# ghcr.io/void-linux/void-glibc-full:latest` (use the manifest digest — the
# same line has to work on x86_64; only glibc x86_64 is published).
# To bump void-packages: set <value> to the new commit; build.sh checks out
# exactly that commit.
base-image docker ghcr.io/void-linux/void-glibc-full@sha256:306f0cdd27a0c35c4a0089318235ce36ee21d43565721376b3f710c6f9780322 void-glibc-full:latest
void-packages version 4384dad29775c7194ff5a7a6054570406ac32e54 void-packages master as of 2026-11-05
+243
View File
@@ -0,0 +1,243 @@
"""Offline policy tests for the Void template of the whisper.cpp binding (#26).
Spec #16 packages three 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.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 mounted files, not an inline sh -c
# string: single quotes inside sh -c silently regroup the command.
for name in ("container-main.sh", "container-build.sh"):
path = VOID_DIR / name
assert path.is_file(), f"missing container script: {path}"
assert name in text, f"{BUILD_SH.name} must mount {name}"