Commit the public half of the repository signing key as packaging/void/repository-key.plist and show its fingerprint beside the registration lines in the root README and the Void packaging README. The test derives the fingerprint from the committed key with the xbps definition (MD5 of the ssh-rsa blob) instead of trusting typed text, checks the key is 4096-bit public material only, and ties the README, the registration URL and the release workflow to it. Clients now register the fork's rolling latest release on Gitea instead of a per-tag upstream GitHub URL, and the first sync no longer passes -y, so an unknown key is never accepted silently. The Void README also documents key generation, the XBPS_SIGNING_KEY secret, the tag workflow and the pending app-template checksum.
251 lines
12 KiB
Markdown
251 lines
12 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.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](#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
|
|
|
|
```sh
|
|
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):
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
# 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:
|
|
|
|
```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, 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):
|
|
|
|
```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 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
|
|
|
|
```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 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.
|