fix(void): package the fork's sources and runtime helpers, not upstream's

The app template fetched upstream's 0.18.5 archive, carried upstream's
homepage and maintainer, and renamed the launchers and desktop entry in
post_install. The fork's sdist already names them voculinux, so the
template now sources the fork's release asset and installs the names as
they are. The checksum is an explicit pending marker until the fork's
v0.18.5 release exists, which fails closed in xbps-src and never falls
back to the upstream digest.

depends now pulls in pipewire (capture), wtype (direct typing on niri),
wl-clipboard (clipboard), xdotool (X11 and XWayland) and pulseaudio-utils
(pactl), and drops ibus, which cannot inject on niri. Every name is
checked against the Void index.

INSTALL.msg states the niri first run plainly (no permission prompt;
typing goes direct), never points at the portal on niri, and drops the
IBus claim. The container smoke queries the fork's distribution name.

Tests assert the fork sources, the pending-or-pinned checksum, the
runtime helpers, the absence of ibus, and the niri wording.
This commit is contained in:
2026-11-06 18:25:22 +05:30
parent e071328b5f
commit 34d33d73bd
5 changed files with 113 additions and 66 deletions
+80 -31
View File
@@ -60,10 +60,32 @@ EXTRA_VOID_DEPS = (
"python3",
"gtk+3",
"libayatana-appindicator",
"ibus",
"gobject-introspection",
)
#: Command-line helpers the app runs at runtime, and the Void package each
#: comes from. The release gate needs them on a packaged install (ticket
#: #27; spec #16 "Void packaging"). ydotool is deliberately absent: it is not
#: packaged for Void, so its fallback rung does not exist there.
RUNTIME_HELPER_PACKAGES = {
"pipewire": "pw-record and pw-dump for PipeWire capture",
"wtype": "direct keysym typing, the first rung on niri",
"wl-clipboard": "wl-copy and wl-paste for the clipboard paths",
"xdotool": "X11 and XWayland injection",
"pulseaudio-utils": "pactl source listing",
}
#: The checksum the template may carry before the fork's release exists. It is
#: explicit and fails closed in xbps-src; it is never the upstream digest.
PENDING_CHECKSUM = "PENDING_FORK_RELEASE_SHA256"
#: sha256 of the upstream 0.18.5 archive. The template must never pin it: the
#: package is the fork's own release, not a copy of upstream's.
UPSTREAM_0_18_5_SHA256 = "1def4d8f0f14f2df2f2fee64ca29e6865e3d82e551f7d465c2a78ec6d4d72b1d"
#: The fork's release host. Every distfile and the homepage live here.
FORK_RELEASE_HOST = "https://git.bongbetic.com/xavierk/voculinux"
#: Every package name the app template may reference, verified to exist in
#: void-packages master on 2026-11-06 via the srcpkgs/<name>/template
#: paths (python3-devel is a subpackage of the python3 template, as
@@ -78,8 +100,12 @@ VERIFIED_VOID_PACKAGES = frozenset(
"python3-cairo",
"gtk+3",
"libayatana-appindicator",
"ibus",
"gobject-introspection",
"pipewire",
"wtype",
"wl-clipboard",
"xdotool",
"pulseaudio-utils",
"python3-numpy",
"python3-requests",
"python3-pysocks",
@@ -208,7 +234,7 @@ def test_template_metadata_shape() -> None:
assert re.fullmatch(r".+ <[^@<>\s]+@[^@<>\s]+>", variables["maintainer"])
assert "noreply" not in variables["maintainer"]
assert variables["license"] == "AGPL-3.0-only"
assert variables["homepage"] == "https://github.com/VocaHQ/vocalinux"
assert variables["homepage"] == FORK_RELEASE_HOST
assert len(variables["short_desc"]) <= 72
assert re.fullmatch(r"[0-9][A-Za-z0-9.]*", variables["version"])
@@ -219,29 +245,35 @@ def test_template_version_matches_version_py() -> None:
def test_distfiles_sources_the_canonical_release_by_checksum() -> None:
"""One release-asset distfile, checksum-pinned; never a git snapshot."""
"""One fork release-asset distfile, checksum-verified; never upstream or git."""
variables = _template_vars()
distfiles = variables["distfiles"]
version = variables["version"]
assert (
"https://github.com/VocaHQ/vocalinux/releases/download/v${version}/" in distfiles
), "distfiles must use the canonical release distribution for this version"
assert distfiles.rstrip().endswith(".tar.gz"), "the release payload is the sdist tarball"
assert distfiles == (
f"{FORK_RELEASE_HOST}/releases/download/v${{version}}/"
f"{variables['pkgname']}-${{version}}.tar.gz"
), "distfiles must be the fork's own release sdist for this version"
assert "github.com" not in distfiles and "VocaHQ" not in distfiles, "no upstream archive"
for moving in ("/master", "/main", "/archive/", "git+"):
assert moving not in distfiles, f"distfiles must not track a moving ref: {moving}"
assert re.fullmatch(r"[0-9a-f]{64}", variables["checksum"]), "checksum must be sha256"
# The unpacked top-level directory must be where the template builds.
# xbps-src moves a single-top-dir archive to pkgname-version itself
# (common/hooks/do-extract/00-distfiles.sh), so the pre-rename archive
# root needs no override — and wrksrc is deprecated, xlint rejects it.
root = Path(distfiles.rsplit("/", 1)[1].replace("${version}", version))
assert root.suffixes[-2:] == [".tar", ".gz"]
assert (
root.name[: -len(".tar.gz")] != f"{variables['pkgname']}-{version}"
), "the release asset is still rooted under the pre-rename archive name"
assert distfiles.rstrip().endswith(".tar.gz"), "the release payload is the sdist tarball"
# The sdist's top-level directory is pkgname-version, so xbps-src needs no
# wrksrc override (deprecated; xlint rejects it).
root = distfiles.rsplit("/", 1)[1].replace("${version}", version)
assert root == f"{variables['pkgname']}-{version}.tar.gz", "archive name must match pkgname"
assert "wrksrc" not in variables, "deprecated wrksrc override: xlint fails the template"
def test_checksum_is_pinned_to_the_fork_release_or_explicitly_pending() -> None:
"""A 64-hex digest once the fork release exists; until then, the explicit
pending marker. Never the upstream digest, never empty."""
checksum = _template_vars()["checksum"]
assert checksum != UPSTREAM_0_18_5_SHA256, "the template must not pin the upstream archive"
assert (
re.fullmatch(r"[0-9a-f]{64}", checksum) or checksum == PENDING_CHECKSUM
), f"checksum must be a sha256 or the explicit pending marker, not {checksum!r}"
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())
@@ -297,22 +329,30 @@ def test_package_ships_no_speech_models() -> None:
assert "model" not in code, "models are not package payload"
def test_post_install_installs_the_renamed_paths_from_the_old_sdist() -> None:
"""The 0.18.5 sdist predates the rename, so post_install promotes the
old names at install time: the entry lands as voculinux.desktop with
rewritten Name/Exec/Icon, and old-prefixed icons land renamed."""
def test_post_install_installs_the_fork_names_without_renaming() -> None:
"""The fork's sdist already carries the new names, so post_install installs
them as they are: no sed rewrite and no mv of upstream launchers."""
body = _post_install_body()
assert "vlicense LICENSE" in body
assert "vocalinux.desktop" in body, "the sdist entry still carries the old name"
assert "voculinux.desktop" in body and "usr/share/applications" in body
assert "s/Vocalinux/Voculinux/" in body, "the entry Name must be rewritten"
assert "s/vocalinux/voculinux/" in body, "the entry Exec/Icon must be rewritten"
assert "vinstall voculinux.desktop 644 usr/share/applications" in body
assert "resources/icons/scalable" in body
assert "usr/share/icons/hicolor/scalable/apps" in body
assert "s/^vocalinux/voculinux/" in body, "old-prefixed icons must land renamed"
assert "usr/bin/vocalinux" in body, "the sdist launchers still carry the old names"
assert "usr/bin/voculinux" in body, "the launchers must land under the new names"
assert "usr/share/voculinux/packaged" in body
for upstream_step in ("sed ", " mv ", "vocalinux"):
assert (
upstream_step not in body
), f"post_install still renames upstream names: {upstream_step}"
assert "usr/bin/vocalinux" not in _template_text(), "no upstream launcher is promoted"
def test_runtime_helpers_are_declared_and_ibus_is_not() -> None:
"""Capture, direct typing, the clipboard and X11 helpers come from xbps;
ibus is excluded because it is not an answer on niri (spec #16)."""
depends = set(_template_vars()["depends"].split())
for package, why in RUNTIME_HELPER_PACKAGES.items():
assert package in depends, f"depends must pull in {package} ({why})"
assert "ibus" not in depends, "ibus is excluded: it cannot inject on niri"
assert "ydotool" not in depends, "ydotool is not packaged for Void"
def test_build_fetches_nothing_beyond_the_sdist() -> None:
@@ -323,11 +363,11 @@ def test_build_fetches_nothing_beyond_the_sdist() -> None:
def test_no_verification_disabling_options() -> None:
"""Dependency verification stays on; distfiles stay checksum-pinned."""
"""Dependency verification stays on; distfiles stay checksum-bound."""
variables = _template_vars()
for key in FORBIDDEN_VARS:
assert key not in variables, f"template sets {key}"
assert re.fullmatch(r"[0-9a-f]{64}", variables["checksum"])
assert "checksum" in variables, "the distfile must carry a checksum line"
def test_template_style_is_lint_clean() -> None:
@@ -348,6 +388,15 @@ def test_install_msg_covers_the_xbps_only_facts() -> None:
for engine in ("vosk", "whisper", "parakeet", "faster-whisper", "onnxruntime"):
assert engine in text, f"optional-engine status must name {engine}"
assert "ydotool" in text, "the absent injection helper must be named"
assert "no permission" in text, "niri first run has no permission prompt"
assert "wtype" in text, "niri types direct through wtype"
assert "ibus" not in text, "IBus is excluded; the message must not point at it"
sentences = re.split(r"(?<=\.)\s+", " ".join(text.split()))
niri_sentences = [sentence for sentence in sentences if "niri" in sentence]
assert niri_sentences, "the message must describe the niri first run"
assert not any(
"portal" in sentence for sentence in niri_sentences
), "the portal is never recommended on niri"
def test_install_msg_line_length() -> None: