Version 0.19.0 across version.py, the Void template and the synced docs; changelog entry; Gitea project URLs in pyproject.toml; per-backend ladder thresholds measured in #19; the xbps release workflow is manual-only, and packaging/void/publish-release.sh publishes a locally built, verified repository to the Gitea releases.
376 lines
21 KiB
Markdown
376 lines
21 KiB
Markdown
# 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-release.sh the release path: gate, clean checkout, sdist, signed repo, install smoke
|
|
├── publish-release.sh publishes a built out-dir to the Gitea release API (dry run first)
|
|
├── container-release.sh the container half: build, sign, install (one per phase)
|
|
├── render-voculinux.sh pins version, _tag and checksum in the build tree only
|
|
├── smoke.sh ticket #28 gate: the release path with a throwaway key
|
|
├── xbps-fingerprint.py the repository key's fingerprint and client plist
|
|
├── 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
|
|
├── 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
|
|
```
|
|
|
|
`srcpkgs/` mirrors void-packages so the tree overlays cleanly:
|
|
`cp -r packaging/void/srcpkgs/. /path/to/void-packages/srcpkgs/`
|
|
(the release path does this inside the container).
|
|
|
|
## Templates
|
|
|
|
| Template | Ticket | Notes |
|
|
|---|---|---|
|
|
| `python3-pywhispercpp` | #26 | Vulkan backend on by default; see below |
|
|
| `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 moves up to the carried
|
|
version, change the carried template and its constraint in the same pull
|
|
request. The live signal is the install smoke: once the stock package reaches
|
|
the carried version, the constraint is already met, the stock package is kept,
|
|
and the pkgver check fails. That is the signal to retire the carried template
|
|
deliberately. The offline stock-version test in `tests/test_void_small_deps.py`
|
|
only records the versions verified on 2026-11-06; it does not query Void.
|
|
|
|
## 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 in a
|
|
void-packages checkout with the overlay applied: `./xbps-src -o ~vulkan pkg python3-pywhispercpp`.
|
|
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.
|
|
- **CPU variants (`#44`)**: `GGML_BACKEND_DL=ON` and
|
|
`GGML_CPU_ALL_VARIANTS=ON` build one CPU library per instruction-set
|
|
level (`libggml-cpu-x64.so` for the plain x86-64 baseline, then
|
|
`haswell`, `skylakex`, `zen4`, ...). ggml loads the best one the host
|
|
supports, so a host with AVX2 runs AVX2 code, and the baseline variant
|
|
ships everywhere. `GGML_NATIVE` stays `OFF`: ggml refuses it with
|
|
`GGML_BACKEND_DL`, and it keeps `-march=native` out of every variant
|
|
(`#938`). `patches/load-ggml-backends-beside-module.patch` makes the
|
|
extension load the variants from its own directory: ggml's default search
|
|
covers the interpreter's directory and the working directory, and neither
|
|
holds them, so without the patch no CPU backend is registered. The install
|
|
smoke checks that `libggml-cpu-x64.so` holds no AVX (`ymm`/`zmm`) code,
|
|
that `libggml-cpu-haswell.so` does, that no variant carries host-native
|
|
tuning (`.GCC.command.line`), and that the CPU and Vulkan backends load
|
|
from the installed package.
|
|
- **Version (`#43`)**: `setup.py` passes no version to `setup()`. The
|
|
version comes from setuptools-scm, which the sdist declares as a build
|
|
requirement, so `hostmakedepends` provides `python3-setuptools_scm` and
|
|
`SETUPTOOLS_SCM_PRETEND_VERSION` pins `1.5.0` without git metadata. Without
|
|
setuptools-scm, setuptools writes `0.0.0`, which the app reads as an
|
|
unknown binding version and then skips device selection. The install smoke
|
|
asserts the installed package reports `1.5.0`.
|
|
- **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.
|
|
The sdist carries no git metadata, which is why the version is pinned
|
|
through setuptools-scm (see Version above).
|
|
|
|
## voculinux
|
|
|
|
The template builds the fork's canonical release source distribution. It is
|
|
built from the release tag, never attached by hand, and never fetched from
|
|
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 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
|
|
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
|
|
install smoke fails the release 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`).
|
|
|
|
### Version mapping
|
|
|
|
xbps versions cannot contain `-`. The xbps spelling of a release is
|
|
`__version__` with the hyphen removed. `build-release.sh names TAG` prints it.
|
|
|
|
| `__version__` (tag `v` + it) | xbps `version` | distfile and release asset |
|
|
|---|---|---|
|
|
| `0.19.0` | `0.19.0` | `voculinux-0.19.0.tar.gz` |
|
|
| `0.19.0-alpha` | `0.19.0alpha` | `voculinux-0.19.0alpha.tar.gz` |
|
|
| `0.19.0-beta` | `0.19.0beta` | `voculinux-0.19.0beta.tar.gz` |
|
|
| `0.19.0-rc1` | `0.19.0rc1` | `voculinux-0.19.0rc1.tar.gz` |
|
|
|
|
Only `X.Y.Z` and `X.Y.Z-alpha`, `-beta` or `-rcN` are release forms; anything
|
|
else is refused. xbps sorts these spellings below the final release
|
|
(`xbps-uhelper cmpver`, checked in `tests/test_void_release_build.py`), so a
|
|
pre-release never looks newer than the release it precedes. The sdist keeps its
|
|
PEP 440 name inside (`voculinux-0.19.0b0/`), which xbps-src's extract hook
|
|
renames to the package's `wrksrc`, so the two spellings need not match.
|
|
|
|
The distfile URL is `releases/download/${_tag}/voculinux-${version}.tar.gz`.
|
|
`_tag` keeps the hyphen (`v0.19.0-beta`), so the URL names the tag that exists.
|
|
|
|
## Checksum
|
|
|
|
The template's `checksum=` is a placeholder of 64 zeros in the committed
|
|
template until the first fork release has a recorded digest. That placeholder is
|
|
never a real digest. Every release build replaces it, in its **build tree only**,
|
|
with the sha256 of the sdist it builds (`render-voculinux.sh`). The release run
|
|
refuses to continue unless exactly one `checksum=` line exists and reads back as
|
|
the computed digest, and it refuses if the committed template changed.
|
|
|
|
After a release, the digest of the published sdist is recorded in the template
|
|
by a **pull request**: the build output and the published release notes both carry
|
|
`checksum=<sha256>`, and the pull request copies it into
|
|
`packaging/void/srcpkgs/voculinux/template`. Nothing commits to `main`.
|
|
|
|
## Building
|
|
|
|
```sh
|
|
just void-smoke # the gate: build, sign with a throwaway key, install, smoke
|
|
just void-release v0.19.0 # build the release set, with XBPS_SIGNING_KEY_FILE set
|
|
packaging/void/build-release.sh names v0.19.0-beta # offline: the names a tag maps to
|
|
packaging/void/build-release.sh check-tag v0.19.0-beta # offline: the tag gate
|
|
packaging/void/publish-release.sh v0.19.0 --out-dir DIR --dry-run # every check, the planned calls, no request
|
|
```
|
|
|
|
Podman only, never Docker. The base image digest and the void-packages commit
|
|
come from `tool_checksums.txt`; bump them there, never in a script.
|
|
|
|
## Smoke gate
|
|
|
|
`packaging/void/smoke.sh` runs the release path for the current version with a
|
|
throwaway RSA 4096 key generated in a temporary directory. The gate needs no
|
|
host Void install and nothing is published. Stages, in order, each fails the run:
|
|
|
|
1. the tag gate (`check-tag`) and the committed template's version and `_tag`;
|
|
2. the canonical sdist, built from the working tree;
|
|
3. `build`: every template in `PKGS` order (`smoke.sh` owns the list), linted
|
|
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, 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, the
|
|
CPU variants with an AVX-free x86-64 baseline, no host-native CPU tuning),
|
|
the installed pywhispercpp reports `1.5.0`, 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.
|
|
|
|
## Signed repository
|
|
|
|
`packaging/void/build-release.sh release TAG --key-file FILE --out-dir DIR`
|
|
is the whole release, and the only path that signs anything. The maintainer runs
|
|
it on the release host (see "Releasing" below); `just void-release` runs the same
|
|
command with `publish/` as the out-dir.
|
|
|
|
Fail-closed, in this order, before any container starts:
|
|
|
|
- the tag must be exactly `v` plus `__version__`, and the committed template must
|
|
name that version and tag;
|
|
- the checkout must be clean: no uncommitted or untracked change. The sdist is built
|
|
from the working tree, so anything else is not the tag. The commit it is built from
|
|
is recorded in `BUILT_FROM`;
|
|
- the key must be non-empty, valid, RSA 4096, and its public half must equal
|
|
`repository-key.plist` byte for byte (a different key never publishes);
|
|
- the canonical sdist must build with Python 3.11 and uv 0.12/0.13. Only its
|
|
wheel is compared byte for byte: a second build of the same tag has identical
|
|
contents, but the tar and gzip timestamps differ, so the release records the
|
|
digest of the sdist it publishes, not of a rebuild.
|
|
|
|
Then three containers, each with only what it needs:
|
|
|
|
| phase | privileges | what runs |
|
|
|---|---|---|
|
|
| build | root, network on, **no key** | void-packages at the pinned commit, the build tree rendered from the tag, every template built as the `builder` user |
|
|
| sign | root, **no network**, key mounted read-only | `xbps-rindex` indexes and signs; no build code runs |
|
|
| install | root, network on (dependencies), **no key** | an unsigned install is refused; the signed repository installs `voculinux`; the smoke above |
|
|
|
|
The committed templates must be unchanged after the build; the run fails if they
|
|
are not. `DIR` must be absent or empty, and the run writes `DIR/sdist/`,
|
|
`DIR/xbps/repo/` (packages, signatures, `x86_64-repodata`), `DIR/xbps/keys/`
|
|
(the client `.plist` named by its fingerprint, and `FINGERPRINT`) and
|
|
`DIR/SDIST_SHA256` and `DIR/BUILT_FROM` (the tag and the commit it was built
|
|
from). A final check accepts only that set: nothing from musl, no other
|
|
architecture, every package signed, the key the pinned one.
|
|
|
|
### The repository key
|
|
|
|
One RSA 4096 key signs everything. Its fingerprint, which clients pin:
|
|
|
|
```text
|
|
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 shows a different one.
|
|
- **Private half**: never committed, never in the repository, never on a
|
|
build host's disk beyond the run. It is kept in the maintainer's own backup,
|
|
and the release reads it as a file on tmpfs (`/dev/shm`) that is removed
|
|
afterwards. The `XBPS_SIGNING_KEY` CI secret holds the same PEM text for the
|
|
workflow, which is manual dispatch only.
|
|
- **Generating a new key** (only if the key is ever replaced; clients then
|
|
have to re-pin, so this is a release-notes event):
|
|
|
|
```sh
|
|
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):
|
|
|
|
```sh
|
|
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 local sequence
|
|
|
|
Pushing a tag starts nothing. `.gitea/workflows/xbps-release.yml` is manual dispatch
|
|
only, kept for a future x86_64 host-mode runner; its one runner today is ARM64 and
|
|
cannot build this release. The sequence is:
|
|
|
|
1. **Merge the release pull request** to `main`. `origin/main` is then the commit to release.
|
|
2. **Create and push the annotated tag** on that commit:
|
|
`git tag -a vX.Y.Z <sha> -m "Voculinux X.Y.Z"`, then `git push origin refs/tags/vX.Y.Z`.
|
|
Never `git push --tags`: the clone holds upstream tags.
|
|
3. **Build in a clean checkout of the tag:** `git worktree add /tmp/voculinux-vX.Y.Z vX.Y.Z`,
|
|
then, in that worktree,
|
|
`packaging/void/build-release.sh release vX.Y.Z --key-file /dev/shm/xbps-sign-key.pem --out-dir /path/to/out`.
|
|
Remove the key file when the run ends.
|
|
4. **Dry-run the publish:** `packaging/void/publish-release.sh vX.Y.Z --out-dir /path/to/out --dry-run`.
|
|
It checks that origin's `vX.Y.Z` peels to the commit in `BUILT_FROM`, that the sdist
|
|
matches `SDIST_SHA256`, and that the client key is the committed one, then prints the
|
|
calls it would make. It sends no request and reads no token.
|
|
5. **Publish:** `packaging/void/publish-release.sh vX.Y.Z --out-dir /path/to/out`. The tag's
|
|
release gets the sdist, packages, signatures, `FINGERPRINT` and the client `.plist`, and
|
|
the rolling `latest` release that the client registration URL serves is replaced, with the
|
|
repodata last. The token comes from `GITEA_TOKEN`, or from the `tea` login for
|
|
`https://git.bongbetic.com`. It goes into a 0600 header file that is removed on exit and is
|
|
never printed.
|
|
6. **Post-release pull request:** record the `checksum=` the build printed in the template
|
|
(section 3.5 of `docs/RELEASE_PROCESS.md`).
|
|
|
|
`publish-release.sh` and the workflow's publish step make the same API calls: the sdist,
|
|
packages, signatures and client key, then the repodata, to the tag's release and then to
|
|
`latest`. Any non-2xx response stops the publish. Publishing an older tag refreshes `latest`
|
|
with the older repository, so publish in version order.
|
|
|
|
Secret setup for the workflow, once: Gitea repository settings, Actions, Secrets, add
|
|
`XBPS_SIGNING_KEY` with the PEM private key. The job token must be allowed to create
|
|
releases and upload release assets.
|
|
|
|
### Runner requirements (the release host)
|
|
|
|
The release runs on the maintainer's x86_64 Void host. The workflow's `runs-on` label
|
|
(`bongbetic-ci`) is ARM64 today and cannot run it. Requirements, which a future x86_64
|
|
host-mode runner would also need. It never uses Docker:
|
|
|
|
| requirement | why | check |
|
|
|---|---|---|
|
|
| `podman` (rootless with `subuid`/`subgid` ranges, or rootful) | the three phases | `podman run --rm alpine true` |
|
|
| unprivileged user namespaces | `xbps-src` builds in a user-namespace chroot (`uunshare`) | the build phase fails with a clear error otherwise |
|
|
| `/dev/shm` mounted as tmpfs | the key must never touch disk | `stat -f -c %T /dev/shm` prints `tmpfs` |
|
|
| `python3.11` on `PATH` | the sdist is built with Python 3.11, as CI does | `python3.11 --version` |
|
|
| `uv` 0.12.x or 0.13.x on `PATH` | the locked release-build tools | `uv --version` |
|
|
| `openssl`, `git`, `sha256sum`, `curl`, `python3` | key checks, checkout, digests, Gitea API | `command -v` |
|
|
| about 25 GiB free disk | the void-packages bootstrap and the whisper.cpp build | `df -h` |
|
|
| egress to `github.com`, `repo-default.voidlinux.org`, `ghcr.io`, `pypi.org`, `files.pythonhosted.org`, `git.bongbetic.com` | clone, bootstrap, base image, locked tools, distfiles, Gitea API | the build phase |
|
|
|
|
A cold build is long: the whisper.cpp Vulkan build is the long step.
|
|
|
|
## Client setup
|
|
|
|
```sh
|
|
# 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 registration text stays correct, and
|
|
`tests/test_void_release_build.py` asserts the release path refuses to
|
|
publish under any other key.
|