Cross-Platform Packaging for Ryll¶
Prompt¶
Before responding to questions or discussion points in this document, explore the ryll codebase thoroughly. Read relevant source files, understand existing patterns (SPICE protocol handling, channel architecture, async task model, image decompression, egui rendering), and ground your answers in what the code actually does today. Do not speculate about the codebase when you could read it instead. Where a question touches on external concepts (SPICE protocol, QEMU, QXL, TLS/RSA, LZ/GLZ compression), research as needed to give a confident answer. Flag any uncertainty explicitly rather than guessing.
Consult ARCHITECTURE.md for the system architecture
overview, channel types, and data flow. Consult AGENTS.md
for build commands, project conventions, code organisation,
and a table of protocol reference sources. Key references
include shakenfist/kerbside (Python SPICE proxy with
protocol docs and a reference client),
/srv/src-reference/spice/spice-protocol/ (canonical SPICE
definitions), /srv/src-reference/spice/spice-gtk/
(reference C client), and /srv/src-reference/qemu/qemu/
(server-side SPICE in ui/spice-*).
When we get to detailed planning, I prefer a separate plan
file per detailed phase. These separate files should be named
for the master plan, in the same directory as the master
plan, and simply have -phase-NN-descriptive appended before
the .md file extension. Tracking of these sub-phases should
be done via a table like this in this master plan under the
Execution section:
| Phase | Plan | Status |
|-------|------|--------|
| 1. Message parsing | PLAN-thing-phase-01-parsing.md | Not started |
| 2. Decompression | PLAN-thing-phase-02-decomp.md | Not started |
| ... | ... | ... |
I prefer one commit per logical change, and at minimum one commit per phase. Do not batch unrelated changes into a single commit. Each commit should be self-contained: it should build, pass tests, and have a clear commit message explaining what changed and why.
Situation¶
Ryll is a Rust SPICE VDI test client with both GUI
(egui/eframe) and headless modes. Today it builds and runs
only on Linux x86_64, using a Docker devcontainer for
reproducible builds. Distribution is manual (scp of a
release binary to the target machine).
Current build infrastructure¶
- Docker devcontainer (
make build,make release) based on Debian with Rust stable - Pre-commit hooks for rustfmt, clippy, shellcheck
- No CI/CD pipeline (no
.github/directory) - No formal packaging for any platform
- No
Cargo.lockin version control
Platform portability issues in existing code¶
-
Signal handling is Unix-only.
src/main.rsuseslibc::signal(libc::SIGINT, ...)for graceful Ctrl+C shutdown. This will not compile on Windows. -
No conditional compilation. There are zero
#[cfg(target_os)]guards anywhere in the codebase. -
Dynamic linking to graphics libraries. On Linux the binary links against libxcb, libX11, libGL, libEGL, libwayland at runtime. On macOS, eframe/egui uses Metal and AppKit natively. On Windows, it uses Direct3D/WinAPI. The egui/eframe crate handles this transparently — no ryll code changes are needed for the graphics backend.
-
openh264 bundles its C library. The
openh264crate (used for--capturevideo encoding) includes Cisco's OpenH264 source and builds it viacc. This should work on all platforms with a C compiler, no system package needed.
Dependencies by platform¶
Linux (Debian/Ubuntu) build-time:
build-essential pkg-config cmake libxcb-render0-dev
libxcb-shape0-dev libxcb-xfixes0-dev libxcb1-dev
libx11-dev libxkbcommon-dev libgl1-mesa-dev
libegl1-mesa-dev libwayland-dev libssl-dev
Linux (Fedora/RHEL) build-time:
gcc gcc-c++ pkg-config cmake libxcb-devel libX11-devel
libxkbcommon-devel mesa-libGL-devel mesa-libEGL-devel
wayland-devel openssl-devel
macOS build-time: - Xcode Command Line Tools (provides clang, Metal SDK) - No additional system packages needed — egui uses Metal backend and AppKit windowing natively
Windows build-time: - MSVC Build Tools (Visual Studio) - No additional system packages — egui uses Direct3D and WinAPI natively
Mission and problem statement¶
Add cross-platform packaging so that ryll can be distributed as pre-built packages for:
- Debian/Ubuntu —
.debpackages - Fedora/RHEL —
.rpmpackages - macOS — Homebrew formula (both Intel and Apple Silicon)
- Windows —
.ziparchive with.exe(and optionally.msiinstaller)
This requires: - Fixing platform portability issues in the source code - Setting up GitHub Actions CI with a multi-platform build matrix - Adding packaging configuration for each target format - Creating a release workflow that builds and publishes packages when a git tag is pushed
Open questions¶
-
Cargo.lock in version control? Rust best practice for binary crates is to commit
Cargo.lock. This ensures reproducible builds across CI and developer machines. We should add it as part of this work. Decision: yes, commit it. -
Minimum macOS version? eframe 0.29 requires macOS 10.14+ (Mojave), released 2018 and long unsupported. Apple currently supports macOS 14 Sonoma (2023), 15 Sequoia (2024), and 26 Tahoe (2025). Decision: set
MACOSX_DEPLOYMENT_TARGET=14.0(Sonoma) as the minimum supported version. This covers all Apple-supported releases. -
Windows capture mode? The
--capturefeature usesopenh264which bundles its own C code viacc. Rather than debugging MSVC build issues for a marginal target, Decision: disable--captureon Windows for now. Feature-gate the capture dependencies (openh264,mp4,pcap-file,etherparse) and capture code behind a Cargo feature (e.g.capture) that is enabled by default on Unix but not on Windows. Can be revisited later. -
Universal macOS binaries? Decision: Apple Silicon (aarch64) only. Apple is dropping security updates for all Intel Macs in 2026. No point shipping x86_64 macOS binaries for a platform with no supported users. Can revisit if someone asks.
-
Homebrew tap vs core? Decision: create a tap (
shakenfist/homebrew-tap). homebrew-core requires popularity and review. Include installation instructions for the tap (and all other pre-compiled package formats) indocs/installation.md. -
RPM: spec file vs cargo-generate-rpm? Decision: use
cargo-generate-rpm. Generates RPMs directly from Cargo metadata, no rpmbuild infrastructure needed. Only revisit with a spec file if we hit a limitation. -
Should we keep the Docker-based build for Linux? Decision: yes, keep the Docker build. The developer uses multiple Rust versions across projects, so the containerised build ensures the right toolchain without polluting the host. CI builds natively (GitHub runners provide the toolchain) to produce distributable artifacts, but the Makefile Docker targets remain the primary local development workflow.
Execution¶
| Phase | Plan | Status |
|---|---|---|
| 1. Platform portability | PLAN-packaging-phase-01-portability.md | Complete |
| 2. GitHub Actions CI | PLAN-packaging-phase-02-ci.md | Complete |
| 3. Debian packaging | PLAN-packaging-phase-03-debian.md | Complete |
| 4. RPM packaging | PLAN-packaging-phase-04-rpm.md | Complete |
| 5. macOS packaging | PLAN-packaging-phase-05-macos.md | Complete |
| 6. Windows packaging | PLAN-packaging-phase-06-windows.md | Complete |
| 7. Release automation | PLAN-packaging-phase-07-release.md | Complete |
Phase 1: Platform portability¶
Make the code compile on Linux, macOS, and Windows.
Changes needed:
- Add
Cargo.lockto version control. - Replace the raw
libc::signal(SIGINT, ...)handler insrc/main.rswith thectrlccrate. It's pure Rust, works on both Unix and Windows, and lets us drop thelibcdependency entirely (nothing else uses it). - Verify the rest of the codebase compiles for
x86_64-apple-darwin,aarch64-apple-darwin, andx86_64-pc-windows-msvctargets (may surface other issues).
Phase 2: GitHub Actions CI¶
Set up continuous integration with a build matrix.
Workflow: ci.yml
- Trigger on push to develop, pull requests
- Matrix: ubuntu-latest, macos-latest (ARM),
windows-latest
- Steps: install platform deps, cargo build --release,
cargo test, cargo fmt --check, cargo clippy
- Cache ~/.cargo and target/ for speed
- Linux job also runs pre-commit run --all-files
Phase 3: Debian packaging¶
Produce .deb packages for amd64.
Approach: Use cargo-deb. Add metadata to Cargo.toml:
[package.metadata.deb]
maintainer = "Michael Still <mikal@stillhq.com>"
section = "utils"
priority = "optional"
depends = "$auto"
assets = [
["target/release/ryll", "usr/bin/", "755"],
]
$auto uses dpkg-shlibdeps to detect runtime library
dependencies automatically.
The CI workflow builds the .deb and uploads it as an
artifact.
Phase 4: RPM packaging¶
Produce .rpm packages for x86_64.
Approach: Use cargo-generate-rpm. Add metadata to
Cargo.toml:
[package.metadata.generate-rpm]
assets = [
{ source = "target/release/ryll", dest = "/usr/bin/ryll", mode = "0755" },
]
Build in CI on ubuntu-latest (cargo-generate-rpm doesn't
need rpmbuild, it generates the RPM directly from the
binary).
Phase 5: macOS packaging¶
Produce binaries for both Intel and Apple Silicon Macs, distributed via Homebrew.
Approach:
- Build on
macos-latest(Apple Silicon) GitHub runner - Create tarball:
ryll-{version}-aarch64-apple-darwin.tar.gz - Create a Homebrew tap repository
(
shakenfist/homebrew-tap) with a formula that downloads the tarball
Phase 6: Windows packaging¶
Produce a .zip archive containing ryll.exe.
Approach:
- Build on
windows-latestwith MSVC toolchain - Package as
ryll-{version}-x86_64-pc-windows-msvc.zip - MSI installer is a nice-to-have for later (requires WiX toolset and more complexity)
Phase 7: Release automation¶
Tie everything together with a release workflow.
Workflow: release.yml
- Trigger on push of a version tag (v*)
- Build all platform artifacts (reuse CI matrix)
- Create a GitHub Release with all artifacts attached:
- ryll_{version}_amd64.deb
- ryll-{version}-1.x86_64.rpm
- ryll-{version}-aarch64-apple-darwin.tar.gz
- ryll-{version}-x86_64-pc-windows-msvc.zip
- Update Homebrew tap formula with new version and SHA256s
Administration and logistics¶
Success criteria¶
We will know when this plan has been successfully implemented because the following statements will be true:
- The code passes
pre-commit run --all-files(rustfmt, clippy with-D warnings, shellcheck). - New code follows existing patterns: channel handler
structure, message parsing via
byteorder, async tasks via tokio, event communication via mpsc channels. - There are unit tests for new logic, and the existing tests
still pass (
make test). - Lines are wrapped at 120 characters, single quotes for Rust strings where applicable.
README.md,ARCHITECTURE.md, andAGENTS.mdhave been updated if the change adds or modifies channels, message types, or compression algorithms.- Documentation in
docs/has been updated to describe any new features or configuration options. Cargo.lockis committed and kept up to date.- CI runs on every push to
developand on pull requests, building and testing on Linux, macOS, and Windows. - A tagged release produces downloadable
.deb,.rpm, macOS tarballs, and Windows.zipartifacts on the GitHub Releases page. - The Homebrew tap formula installs a working
ryllon macOS.
Future work¶
- Universal macOS binary — use
lipoto combine x86_64 and aarch64 into a single universal binary. - MSI installer for Windows — use WiX toolset to produce a proper Windows installer with Start Menu shortcuts.
- AUR package — Arch Linux user repository package.
- Flatpak/Snap — containerised Linux distribution.
- ARM Linux builds — aarch64 Linux binaries for Raspberry Pi / ARM servers.
- Automated Homebrew tap updates — GitHub Action that auto-updates the formula on new releases.
- Code signing — sign macOS and Windows binaries to avoid security warnings.
- Static musl builds — fully static Linux binaries for headless-only use (no GUI support with musl).
- Reproducible builds — ensure byte-identical output across build environments.
Bugs fixed during this work¶
(None yet.)
Back brief¶
Before executing any step of this plan, please back brief the operator as to your understanding of the plan and how the work you intend to do aligns with that plan.