rb-cli on MiSTer (armv7-unknown-linux-gnueabihf)

June 27, 2026 · View on GitHub

Idea: cross-compile a stripped-down rb-cli for the MiSTer's on-board Linux so the Wave-2 [!] ref user-side boot-test parks can be validated in-place on the device instead of round-tripping through scp + a host machine.

Target environment (per the user's MiSTer):

Linux 5.15.1-MiSTer #6 SMP Thu Jul 4 01:01:19 UTC 2024 armv7l GNU/Linux

— Intel Cyclone V SoC, dual-core Cortex-A9 ~800 MHz, glibc Buildroot rootfs (~256 MB), USB / SD writable storage under /media/fat/.

This file is the idea capture for a later session. Nothing has been built yet.


Why this matters

The [!] markers still open on Wave-2 spine rows after the 2026-06-05 close-out are:

CoreOpen user-side parks
X68000ref (Workflow A+C+B), write-verified, gui
Archieref (Workflow A+C), write-verified, gui
QLref (Workflow A+C; sQLux passed, hardware doesn't), gui
Altairgui

Workflow A+C is "build a test image on a host, scp to MiSTer, mount in the core, verify the file is visible and readable." Workflow B reverses it ("write from inside the core, power down, pull SD, check host-side with rb-cli").

Today the only way to drive these workflows is:

host:  rb-cli put / get / ls    →    scp    →    MiSTer core mount

Every iteration is a round-trip. If rb-cli ran on the MiSTer itself, the loop collapses to:

MiSTer shell:  rb-cli put / get / ls    →    core mount (same SD)

— closer to the way real users work, and the same binary is then usable for ad-hoc inspection / extraction of any of the dozen+ disc-image formats rusty-backup understands without leaving the device.

It also gives us a closer-to-real-hardware oracle for the Archie HDD resize work that's currently deferred (OPEN-WORK §7) — if rb-cli is on the MiSTer, a resize test means "shrink/grow the HDF, unplug, plug back in, boot the core" rather than booting RPCEmu on a host.


Blockers (concrete, all surveyed 2026-06-05)

1. libchdman-rs with the prebuilt feature

Cargo.toml line 66:

libchdman-rs = { version = "0.288.1", features = ["prebuilt"] }

The prebuilt feature pulls a precompiled CHD core; upstream's prebuilt matrix doesn't currently include armv7-unknown-linux- gnueabihf. So the link step will fail.

Resolutions, easiest first:

  • (A) Drop CHD for the MiSTer build — gate libchdman-rs behind a chd feature flag, off by default for a new minimal-cli build profile. ADFS / QXL.WIN / .d88 / .adf / .hdf / .vhd / .img / .zst all still work without CHD. The use cases where MiSTer-side CHD matters are vanishingly small (Cortex-A9 is too slow for CHD compression to be useful anyway).
  • (B) Build CHD from source on the host cross-toolchainlibchdman-rs is a Rust wrapper around the MAME CHD core; with the C++ source vendored it should cross-compile via cc + a proper ARM gnu++ toolchain. Half a day of work to validate.
  • (C) Ship an armv7 prebuilt upstream — extend libchdman-rs's prebuilt feature to publish an Ubuntu-20.04-compatible armv7 binary that matches the MiSTer Buildroot glibc baseline. Same shape as the existing x64 / arm64 prebuilts; no compile-from-source on the consumer side.

Recommendation: (C), pursued upstream. Keeps CHD on the MiSTer without dragging the MAME C++ toolchain into rusty-backup. The chd feature flag from (A) still ships (the desktop build sometimes wants to opt out for licensing reasons), but the MiSTer artifact turns it ON via --features chd. The repo's Cross.toml pins the cross-compile image to Ubuntu 20.04 so symbols line up with the upstream prebuilt.

2. GUI deps drift into rb-cli via the workspace lib

The rb-cli binary lives at src/bin/rb_cli.rs and pulls everything from src/lib.rs. The lib re-exports the GUI module unconditionally, so building rb-cli still resolves:

  • eframe = "0.34" (egui's eframe runner)
  • egui = "0.34"
  • rfd = { version = "0.17", features = ["gtk3"] }

On MiSTer Buildroot, GTK3 dev libs are not present, so rfd fails at link. egui itself is pure Rust but pulls in glow (OpenGL via GLES2 bindings) which on Linux wants libGL + libEGL headers — also probably absent.

Resolution: feature-gate the GUI. Introduce a gui feature in the lib Cargo.toml, off by default for the rb-cli build:

[features]
default = ["gui", "chd"]
gui = ["dep:eframe", "dep:egui", "dep:rfd"]
chd = ["dep:libchdman-rs"]
minimal-cli = []  # nothing

And make src/gui/, src/main.rs, and the rb-cli code paths that currently call into GUI types compile out under #[cfg(feature = "gui")] / #[cfg(not(feature = "gui"))].

The split has to be careful — rb-cli today uses a handful of model types (InspectTab::*, BrowseView::*) that live in the GUI module even though they're logically core. Those need to move down to src/model/ before the cut.

3. Native C-lib dependencies — cross-toolchain setup

Workspace deps that have C / C++ components:

CrateC component
flate2 (zlib-ng)zlib-ng (cmake + cc)
zstd-syszstd (cc)
bzip2-sysbzip2 (cc)
sha2optional asm; pure-Rust path works on arm
reqwestrustls (pure Rust) or openssl-sys
crc32fastoptional simd; portable fallback works
aespure Rust
imageoptional PNG via image-png (pure Rust)

All of these are well-known to cross-compile under cross (https://github.com/cross-rs/cross) with the default armv7-unknown-linux-gnueabihf image. The lone risk is zlib-ng (depends on which cmake the image ships); the fallback is switching flate2's default features from zlib-ng to the pure-Rust rust_backend.

For reqwest: switch features from default (openssl-sys via native-tls) to rustls-tls so there's no openssl-sys dep.

4. opticaldiscs / cd-da-reader (drives feature)

opticaldiscs (CHD/ISO/BIN-CUE disc-image browse + drive enum) and cd-da-reader (physical-drive ripping). The original worry here — "libcdio may not be present in the Buildroot rootfs" — turned out to be unfounded: cd-da-reader is cc + libc only (a self-compiled SG_IO ioctl shim, no system libcdio link), and opticaldiscs's drives feature is pure Rust.

Resolution (updated 2026-06-26): SHIPPED on armv7. The real blocker was that opticaldiscs pinned libchdman-rs 0.287.0-l7, which publishes no armv7 prebuilt (only x86_64 / aarch64 / apple / windows). opticaldiscs 0.4.5 bumps that to libchdman-rs 0.288.5 — the same version the chd feature uses, which does ship an armv7-unknown-linux-gnueabihf-glibc2.31 prebuilt — so the two dedupe to one libchdman and the optical stack cross-compiles. The MiSTer job now builds with optical enabled; it stays gated behind the optical feature (default on for host builds).


Proposed cuts

Introduce three workspace features:

[features]
default = ["gui", "chd", "optical"]
gui = ["dep:eframe", "dep:egui", "dep:rfd"]
chd = ["dep:libchdman-rs"]
optical = ["dep:opticaldiscs", "dep:cd-da-reader"]
minimal-cli = []  # nothing — only the CLI binary and its core deps

rb-cli becomes buildable via:

cargo build --bin rb-cli --no-default-features

On the host this is a fast headless build (no glow / GTK link); on the MiSTer cross-target it's the only path that resolves.

Reqwest also flips:

reqwest = { version = "0.13", default-features = false,
            features = ["blocking", "json", "rustls-tls"] }

(host build doesn't need native-tls either — saves the openssl-sys dep for everyone.)


Build incantation (host or CI)

# Linux x64 host with Docker + cross:
cargo install cross --git https://github.com/cross-rs/cross --locked

# From the rusty-backup repo root — Cross.toml pins this target's image
# to cross-rs's Ubuntu 20.04 / glibc 2.31 build (digest-pinned), which
# matches both the MiSTer Buildroot rootfs and the upstream libchdman-rs
# armv7 prebuilt's libstdc++.
cross build --target armv7-unknown-linux-gnueabihf \
            --bin rb-cli \
            --release \
            --no-default-features --features chd,pure-zstd

# Output: target/armv7-unknown-linux-gnueabihf/release/rb-cli
file target/armv7-unknown-linux-gnueabihf/release/rb-cli
# expect: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV),
#         dynamically linked (loader /lib/ld-linux-armhf.so.3)

# Sanity check it links against glibc 2.x (MiSTer ships 2.31+):
arm-linux-gnueabihf-objdump -p target/.../rb-cli \
  | grep -i 'NEEDED\|GLIBC_'

# Deploy. The CI release tarball ships the binary as `rb-cli-mini`; do
# the local rename here too so the device-side filename matches the
# downloads-page artifact and so `rb-cli-mini install-completions`
# writes its script to bash-completion/completions/rb-cli-mini (which
# is where bash looks when the user types `rb-cli-mini<TAB>`).
scp target/armv7-unknown-linux-gnueabihf/release/rb-cli \
    root@mister.local:/media/fat/Scripts/rb-cli-mini
ssh root@mister.local 'chmod +x /media/fat/Scripts/rb-cli-mini'

# Smoke test (replace with a real image already on the SD):
ssh root@mister.local '/media/fat/Scripts/rb-cli-mini inspect /media/fat/games/Archie/CROS42.hdf'

Strip the binary for size:

arm-linux-gnueabihf-strip target/.../rb-cli
# Expect ~15 MB stripped with chd + optical (libchdman's static C++ CHD core
# dominates; it was ~5-8 MB before CHD/optical were added to the mini build).

What this would unblock

Driving each Wave-2 user-side [!] ref mark to [x] becomes practical (single-device workflow):

CoreOn-device workflow
X68000rb-cli put <foo.d88> X68000/games.d88 BAR → mount core → dir A:type BAR
Archierb-cli put CROS42.hdf Apps/test "hello" → reboot Archie core → *Type test
QLrb-cli put WIN.win win1_/BAR "..." → boot QL → DIR win1_LOAD win1_BAR
Altairrb-cli ls disc.dsk → boot CP/M → DIR cross-check

Each is now a single-shell loop instead of a host ↔ device shuffle.

The same binary is also useful for anyone using the MiSTer to play with vintage disc images — inspect, browse, extract individual files — without needing a separate computer. That's beyond the Wave-2 scope but the natural fallout.


Estimated effort

Rough ranges, all assuming the feature-flag plumbing goes cleanly:

PhaseEffort
Carve gui/chd/optical features in Cargo.toml1 session (~2h)
Find + relocate GUI-coupled model types out of src/gui/1-2 sessions
cargo build --no-default-features clean on x64 host1 session
cross build --target armv7-... clean1 session
Smoke-test on MiSTer hardware (user-side)1 session
Wire CI matrix entry for armv7 release artifact1 session (opt.)

Realistic landing: 3-5 focused sessions for "buildable + tested", plus optional CI. The first session (feature plumbing) is the biggest unknown — depends on how much GUI / core coupling has crept in.


Open questions for the picking-up session

  1. How tangled is src/model/ vs src/gui/? A grep through src/bin/rb_cli.rs for gui:: and model:: references will tell us how big the cleanup is before the feature flag works.
  2. Does the rb-cli command surface need anything from libchdman- rs? If rb-cli backup --to chd is a thing, the --no-chd build needs to error out cleanly on that verb rather than silently producing a corrupt file.
  3. Distribution channel? Drop the armv7 binary into the regular GitHub release matrix, or keep it ad-hoc? The MiSTer audience is small enough that ad-hoc is probably fine for v1.
  4. glibc floor? MiSTer's Buildroot tracks recent glibc; check what ldd --version reports on a live MiSTer before fixing a minimum target.
  5. Static-link alternative? armv7-unknown-linux-musleabihf would produce a fully static binary that runs without worrying about glibc compatibility. Trade-off: musl + Rust + zstd-sys sometimes has performance issues. Not worth it unless the glibc story turns out to be painful.

Status

Idea captured 2026-06-05 after the Wave-2 Archie engine close-out. Shipped 2026-06-06:

  • Cargo.toml carries three optional features — gui (eframe / egui / rfd / reqwest / webbrowser), chd (libchdman-rs), and optical (opticaldiscs / cd-da-reader). default = ["gui", "chd", "optical"] keeps the desktop release a single binary; the slim build is cargo build --bin rb-cli --no-default-features.
  • reqwest flipped from native-tls to rustls, so the host build no longer drags in openssl-sys.
  • Module-level gates throughout src/: src/main.rs (gui), src/update.rs, src/model/{update_runner,chd_expand_runner}, src/optical/, src/cli/verbs/optical.rs, src/rbformats/{chd,chd_edit,chd_options}, src/backup/single_file_chd. Runtime stubs in src/rbformats/mod.rs keep call sites (e.g. BackupConfig::chd_options, BrowseSession::chd_edit_session) compiling; calls into the stubs return a clear "this binary was built without the chd feature" error.
  • CI workflow .github/workflows/release.yml ships a new build-rb-cli-mini-armv7 job that uses cross to produce rb-cli-mini-armv7-linux-<version>.tar.gz as a release artifact.
  • Cross.toml pins the armv7 cross-compile image to cross-rs's Ubuntu-20.04-based build (GCC 9.4, glibc 2.31), referenced by SHA digest. This matches the glibc + libstdc++ baseline both the MiSTer Buildroot rootfs and the upstream libchdman-rs armv7 prebuilt use. Note: cross-rs's last semver release (v0.2.5) is Ubuntu 16.04 / GCC 5.4 / glibc 2.23 — too old to have the C++11 std::thread polymorphic-_State symbols the prebuilt depends on, so we pin to a digest of the :main rolling tag rather than v0.2.5.
  • The MiSTer job builds with --no-default-features --features chd,pure-zstd,remote,optical so the artifact includes CHD support via the libchdman-rs prebuilt (plus the pure-Rust zstd backend — exactly one zstd backend is required, and a cross build can't link C libzstd), the rb-daemon (remote), and the optical stack (optical — CD/DVD ripping; requires opticaldiscs >= 0.4.5 so both it and chd share the armv7 libchdman-rs 0.288.5 prebuilt). Only the GUI stays out. The job is marked continue-on-error: true and is intentionally OUT of the release job's needs: list, so a transient cross-compile failure (or the timing window while upstream libchdman-rs armv7 prebuilt is in flight) does not block the desktop releases.
  • README has a rb-cli-mini section with the build incantation and the included/excluded feature matrix.

Remaining open: smoke-test the produced armv7 binary on a real MiSTer device (the X68000 / Archie / QL Workflow A+B+C parks in §7 of OPEN-WORK) — this requires hardware access and is tracked there rather than here.