Files
voculinux/packaging/void
xavierk 05ef78922d fix(void): build the xbps release from the tag, not an attached sdist
The signed Void release depended on a fork sdist attached to the release
before the tag existed, and the app template carried a PENDING checksum that
had to be filled by hand. Every release is now built from its tag alone:

- packaging/void/build-release.sh is the one release path. The Gitea workflow,
  the local dry run and the smoke gate all call it.
- The canonical reproducible sdist is built from the tag with
  scripts/build_reproducible_wheel.py (Python 3.11, pinned uv).
- render-voculinux.sh pins version, _tag and checksum in the container's build
  tree only, and places the sdist where xbps-src resolves its distfile.
  The committed template keeps a zero placeholder until the post-release
  checksum pull request.
- xbps version is __version__ with the hyphen removed (0.19.0-beta becomes
  0.19.0beta), which xbps sorts below the final release.
- Builds run without the key; signing runs with the key mounted read-only and
  no network; the install smoke runs in a clean image without the key.
- The workflow gates the tag and the key before any container starts, writes
  the key to a 0600 tmpfs file, and publishes only the staged set (sdist,
  packages, signatures, repodata, client key).
- smoke.sh runs the same pipeline with a throwaway key.
2026-11-06 20:06:35 +05:30
..

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
├── smoke.sh                   ticket #28 gate: every template, one command
├── repo.sh                    ticket #29 pipeline: build all, index, sign
├── container-repo.sh          ticket #29 container half (index + sign + verify)
├── repository-key.plist       the repository's PUBLIC key (never a private key)
├── 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
voculinux #27 The app, from the canonical release sdist; 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.

voculinux

Version 0.18.5, matching src/vocalinux/version.py — never newest upstream. The template builds the fork's canonical release source distribution, voculinux-0.18.5.tar.gz, attached to the fork's v0.18.5 release on git.bongbetic.com, with a pinned checksum (see Checksum); tests/test_void_app_template.py fails if the version drifts from version.py.

  • 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 (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 container build re-verifies via dependency resolution.
  • No payload beyond the app: no speech models (they download on first run), no vendored wheels, no pip at build or install time. The container smoke fails the build if the payload carries model weights or wheels.
  • Installed names: the voculinux.desktop entry, the hicolor icons, and a usr/share/voculinux/packaged marker the app reads to present package-manager updates. The fork's sdist already carries the final names, so the template installs them as they are; the payload smoke rejects any pre-rename leftovers in the OS-visible paths.
  • INSTALL.msg covers what xbps cannot express: the input group for the evdev hotkey backend, first-run model downloads, optional engines as manual additions (vosk, OpenAI whisper, Parakeet, faster-whisper, onnxruntime), the niri first run, the absent ydotool rung, and updating through xbps.
  • Update checker: packaged installs keep background checks as a signal, but every update affordance (tray entry, notification, About page) presents the package-manager path (src/vocalinux/utils/packaging.py).

Checksum

The template's checksum= is PENDING_FORK_RELEASE_SHA256 until the fork's v0.18.5 release asset (voculinux-0.18.5.tar.gz on git.bongbetic.com/xavierk/voculinux) exists. Once it does, set checksum= to the sha256sum of that exact downloaded file, never a value copied from upstream, and land the change in a pull request. Until then xbps-src fails closed on the pending value, so neither the smoke gate nor the signed repository can build voculinux.

Building

packaging/void/build.sh python3-pywhispercpp
packaging/void/build.sh python3-pywhispercpp -- -o ~vulkan pkg
packaging/void/build.sh voculinux

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.

Smoke gate

One command runs the whole gate from a clean checkout — no host Void install — linting, building, installing and smoke-testing every template under srcpkgs in dependency order (the app last):

packaging/void/smoke.sh
# or: just void-smoke

Each package delegates to build.sh, so the pinned container and the per-package stages (xlint, xbps-src, install, payload/version/import smoke) stay implemented once. The gate fails fast: a broken package prints FAIL: <pkgname> naming which package broke, above it the harness echoes which stage broke. tests/test_void_smoke_gate.py asserts offline that the gate stays wired to every srcpkgs template, so a renamed or added template cannot silently drop out.

Signed repository

packaging/void/repo.sh builds every template in smoke-gate order, indexes the full repository (x86_64-repodata), and signs every binary package (*.sig2) plus the repository metadata with the RSA 4096 repository key — one command, same pinned container via podman, no host Void install:

# Run the smoke gate first: the repo assembles tested packages.
packaging/void/smoke.sh
# or: just void-smoke

printf '%s' "$XBPS_SIGNING_KEY" > /tmp/xbps-sign-key.pem
XBPS_SIGNING_KEY_FILE=/tmp/xbps-sign-key.pem packaging/void/repo.sh
# or: XBPS_SIGNING_KEY_FILE=/tmp/xbps-sign-key.pem just void-repo

The pipeline stages publish/xbps/repo/ (packages, signatures, repodata) plus publish/xbps/keys/ (the client .plist named by key fingerprint, and a FINGERPRINT file carrying the signer identity and fingerprint for release notes). publish/ is local build output and stays uncommitted.

Fail-closed: a missing, unreadable, or non-RSA4096 key aborts before any container runs, so an unsigned repository can never publish.

The repository key

One RSA 4096 key signs everything. Its fingerprint, which clients pin:

fa:d1:00:32:ce:b1:b9:fd:73:05:05:76:75:54:37:25
  • Public half: committed as packaging/void/repository-key.plist (the same file xbps keeps in /var/db/xbps/keys/, named by the fingerprint). Signer identity in the file: Bongbetic <soubarna@live.in>. tests/test_void_signed_repo.py derives the fingerprint from this file and fails if either README, or the release workflow, shows a different one.
  • Private half: never committed, never in the repository, never on a build host's disk beyond the run. It exists as the XBPS_SIGNING_KEY CI secret (PEM text, newlines intact) and in the maintainer's own backup.
  • Generating a new key (only if the key is ever replaced; clients then have to re-pin, so this is a release-notes event):
openssl genrsa -out xbps-sign-key.pem 4096
# back the key up somewhere safe, then store it as XBPS_SIGNING_KEY
openssl rsa -in xbps-sign-key.pem -pubout    # the public half, to build the .plist from
  • Checking a private key against the committed public key, before it is stored as the secret (both lines must print the same digest):
openssl rsa -in xbps-sign-key.pem -pubout 2>/dev/null | openssl md5
python3 -c "import plistlib,sys; sys.stdout.buffer.write(plistlib.load(open('packaging/void/repository-key.plist','rb'))['public-key'])" | openssl md5

Releasing: the Gitea workflow

A human pushes a version tag; .gitea/workflows/xbps-release.yml does the rest. It never runs on a schedule or a manual dispatch.

  1. The tag must equal v plus __version__ in src/vocalinux/version.py, exactly; any other tag exits 1 and publishes nothing.
  2. The CI secret XBPS_SIGNING_KEY must be set; empty exits 1 before any container runs. The key is written to a 0600 temp file, handed to repo.sh as XBPS_SIGNING_KEY_FILE, and removed by a final step that always runs.
  3. repo.sh builds and signs inside the pinned container (podman, never Docker).
  4. The output is checked: every class of file present (*.x86_64.xbps, *.sig2, *-repodata*, FINGERPRINT, the .plist), every package signed, nothing but glibc x86_64 data, and the signing key's fingerprint equal to the one above.
  5. Everything attaches to the tag's release and to the rolling latest release (replacing that release's old assets), through the Gitea API. latest is what the client registration URL serves.

The workflow never commits or pushes. Bumping the templates to the new release is a normal pull request (docs/RELEASE_PROCESS.md).

Secret setup, once: Gitea repository settings, Actions, Secrets, add XBPS_SIGNING_KEY with the PEM private key. The runner (bongbetic-ci) needs podman (the workflow installs it with apt when absent), and the job token must be allowed to create releases.

Client setup

# as root on the Void system
curl -fsSL -o '/var/db/xbps/keys/fa:d1:00:32:ce:b1:b9:fd:73:05:05:76:75:54:37:25.plist' \
  https://git.bongbetic.com/xavierk/voculinux/raw/branch/main/packaging/void/repository-key.plist
echo 'repository=https://git.bongbetic.com/xavierk/voculinux/releases/download/latest' > /etc/xbps.d/voculinux.conf
xbps-install -S voculinux

The key file is named by its fingerprint, so xbps finds it on the first sync. If xbps shows a fingerprint prompt anyway, answer N unless it reads exactly the fingerprint above and matches the FINGERPRINT file on the release; do not add -y to the first sync. tests/test_void_signed_repo.py asserts offline that the pipeline stays wired to every srcpkgs template, stays podman-only on the pinned inputs, and keeps the fail-closed key contract; tests/test_xbps_release_workflow.py runs the workflow's gates against fixtures.