Pip-installable ryll (ryll)¶
This is phase 3 of a cross-repository master plan whose plan of record lives in the Shaken Fist repository:
shakenfist/docs/plans/PLAN-kerbside-vdi-tokens.md(branchvdi-console-tokensuntil merged)
It is self-contained and can land independently of the other phases.
Prompt¶
Make ryll pip-installable so that pip install ryll (typically pulled
in via the client's shakenfist-client[vdi] extra in phase 4) puts a
working ryll executable on the venv's PATH, the way pip install
kerbside ships kerbside-proxy. Phase 4's sf-client instance
vdiconsole then launches ryll with the kerbside exchange URL and the
user lands in a SPICE session with none of the token plumbing visible.
This phase absorbs the packaging complexity so phase 4 sees only a
binary that is or is not on PATH.
Situation¶
- ryll is a GUI binary in a Cargo workspace. The
ryllcrate (ryll/Cargo.toml) is the SPICE VDI client; default features["capture", "gui", "audio"]pull ineframe(winit/wayland/xkb),arboard,rfd, andcpal/opus. A--no-default-featuresbuild is headless, which is not what a user needs to see a console. - ryll builds in Docker; host policy is no native Rust toolchain.
- ryll already has a release pipeline (
.github/workflows/release.yml, onv*tags): per-target.deb/.rpm/macOS-.tar.gz/Windows-.zipattached to a GitHub Release, plusmake publish-cratesto crates.io.check-versionenforces that the tag equals[workspace.package].version. - The kerbside-proxy precedent ships a maturin
bindings = "bin"wheel that lays the compiled binary into the wheel's*.data/scripts/so pip puts it on PATH. kerbside-proxy is headless, so its--zig --compatibility manylinux_2_28build both links and tags cleanly.
Mission and problem statement¶
Deliver a PyPI package ryll such that, in a venv, pip install
ryll (or pip install shakenfist-client[vdi]) yields a ryll on PATH
that phase 4 can launch with ryll --url <…>, with no runtime download
and working offline immediately after install. Contain all packaging
complexity here; phase 4 sees only "ryll present or not" and degrades
to remote-viewer otherwise.
Alternatives considered¶
A. Embed the binary in per-arch manylinux wheels — chosen¶
Build the ryll GUI binary inside a manylinux_2_28 container that has
the GUI/audio dev libraries installed, and ship it with maturin
bindings = "bin" as a per-arch platform wheel
(manylinux_2_28_x86_64 / _aarch64). pip install ryll puts the
binary straight on PATH; nothing is fetched at runtime. Chosen for a
self-contained, offline-capable install (operator preference,
2026-07-20). The --zig shortcut kerbside-proxy uses does not
work here (zig's sysroot lacks the GUI/audio dev libs), so the build
runs natively inside the manylinux container instead — see Decisions.
B. Fetch the release binary at first run — rejected¶
ryll as a pure-Python py3-none-any wheel whose console-script
downloads + SHA256-verifies the native release binary on first run.
Fully built and working at one point, then dropped in favour of A:
embedding avoids a runtime GitHub dependency, works offline right after
pip install, and needs no launcher/download/verify code on every
user's machine. The cost A carries instead — a manylinux build lane and
larger per-arch wheels — lives in our CI, not on users.
C. Compile from source at pip-install time (maturin sdist) — rejected¶
Requires a Rust toolchain and every GUI/audio -dev library on the
user's machine at install; catastrophic UX and unusable in CI.
Decisions¶
- Package name
ryll(confirmed available on PyPI, 2026-07-20), console-scriptryll. Distributed via theshakenfist-client[vdi]extra in phase 4. - maturin
bindings = "bin"fromryll/pyproject.toml, with an explicit[[bin]] name = "ryll"inryll/Cargo.toml. Version isdynamic = ["version"], inherited from the crate'sversion.workspace = true(maturin resolves the workspace inheritance) — no stamping step, andcheck-versionalready enforces tag == version. - Build natively inside
quay.io/pypa/manylinux_2_28_<arch>(AlmaLinux 8, glibc 2.28), not via--zig. The spike (2026-07-20) proved the recipe end to end; it lives intools/build-ryll-wheel.sh(host wrapper, per arch) +tools/build-ryll-wheel-in-container.sh(in-container steps): enable EPEL + PowerTools,dnf installthe C/C++ toolchain and the GUI/audio-devellibs, install rustup stable +maturin>=1.7,<2, thenmaturin build --release --compatibility manylinux_2_28 --skip-auditwheel --out target/wheels. --skip-auditwheelis correct, and the manylinux_2_28 tag is honest.readelf -don the built binary shows the only non-standardDT_NEEDEDislibasound.so.2, and the max glibc symbol version is 2.28. The GUI/audio libraries (libGL/EGL, wayland, xcb, xkbcommon, X11, opus) are dlopen'd at runtime, not linked, so there is nothing for auditwheel to vendor and the tag does not overpromise. Proven: on a cleanubuntu:22.04(glibc 2.35),pip install ryll-0.1.5-py3-none-manylinux_2_28_x86_64.whlthenryll --versionprintsryll 0.1.5and exits 0.- Runtime system libraries. ryll is a Linux GUI app, so the binary
needs
libxcb1,libwayland-client0,libxkbcommon0,libasound2,libopus0,libgl1present to run its GUI — pip cannot install these under any packaging model. Documented in the README; phase 4'sremote-viewerfallback covers hosts that lack them. - Per-arch, Linux only. Wheels for
x86_64andaarch64, built on native runners of each arch (x86_64 on the self-hosted docker VM, aarch64 on a github-hosted arm runner — no QEMU-emulated compile). Other platforms (macOS, Windows, musl, glibc < 2.28) get no wheel and use a system package / release artifact / build-from-source. - Publish + release gating (parity with kerbside). Publish via PyPI
Trusted Publishers (OIDC, no tokens), staged TestPyPI → PyPI. All
publishing (
publish-crates,github-release, and theryllPyPI jobs) is gated on asign-tagjob that keyless-Sigstore-signs the release tag behind a manual-approvalreleaseGitHub Environment, so nothing publishes before the approved, signed tag exists. Required one-time setup (cannot be done from code): (a) areleaseenvironment in the ryll repo settings with required reviewers, or the gate is a no-op; (b) a pendingryllTrusted Publisher on both pypi.org and test.pypi.org (Ownershakenfist, Reporyll, Workflowrelease.yml, environmentrelease).
Execution¶
Implemented in the ryll-wt-vdi-tokens worktree (branch
vdi-console-tokens).
| Step | Status | What |
|---|---|---|
| Embed recipe spike | Done 2026-07-20 | Proved the native manylinux_2_28 maturin bindings=bin build for the GUI binary; wheel installs and runs on a clean host (Decision 4). |
| Packaging | Done | ryll/pyproject.toml, [[bin]] in ryll/Cargo.toml, tools/build-ryll-wheel.sh + tools/build-ryll-wheel-in-container.sh. |
| CI/release | Done | build-ryll-wheels per-arch matrix; publish-testpypi → publish-pypi of the wheels, gated on sign-tag + release environment; fetch-only tarball/checksums/python-test jobs removed. |
| Docs | Done | README "Installing via pip" + docs/installation.md rewritten for the embed model. |
Success criteria¶
pip install ryllon Linux x86_64/aarch64 (glibc ≥ 2.28) installs a wheel that already contains the binary;ryll --versionworks with no network and no compile step.- The wheel's
manylinux_2_28tag is honest (glibc floor 2.28; onlylibasound.so.2beyond the standard set inDT_NEEDED). - A missing runtime GUI library is the user's to install (documented);
phase 4 falls back to
remote-viewer. - CI publishes the
ryllwheels after the approved, Sigstore-signed tag, staged through TestPyPI, via Trusted Publishers. - Phase 4 can rely on
ryllbeing on PATH after the[vdi]extra, and on graceful absence otherwise.
Agent guidance¶
- The binary is a GUI app: a headless CI runner can build and
--version-smoke it (that only loadsDT_NEEDED), but cannot open a window. Don't assert on GUI behaviour in CI. - The aarch64 wheel must build on a native arm runner; do not QEMU-emulate the compile (far too slow).
--skip-auditwheelis intentional (Decision 4); do not "fix" it by adding an auditwheel-repair step — it would reject the dlopen'd GUI libs.
Administration and logistics¶
- Cross-repo sequencing: phase 4 (client) depends on this phase but
degrades gracefully via
remote-viewerif it slips. Nothing here depends on the Shaken Fist phases 1-2 already landed. - The shakenfist master plan's phase-3 row is set to In progress while this lands.