Ship the reader wheels inside the package for offline install

A device with no internet could not set itself up: the auto-install needed
PyPI. The rdm6300 and pyserial wheels now ship inside the package, so first
start works with no network at all.

Both wheels are pure Python (py3-none-any / py2.py3-none-any), which is what
makes this possible - the same files work on armv7l, aarch64 and x86_64. This
is the difference from the workstation app's Files/repository cache, which
held aarch64 manylinux binaries and so could not be committed; that cache only
ever existed on the Pi devices.

- bundled_wheel_dir() locates them; ensure_dependencies() prefers them
  automatically, so the service needs no --wheel-dir.
- verify_wheels() checks SHA256SUMS before installing. A wheel that fails its
  checksum is refused rather than installed, then PyPI is tried, and if that is
  unreachable it reports exit 2 instead of proceeding unverified.
- local_wheel_missing() falls back to PyPI when the bundle does not cover a
  requirement, instead of attempting a --no-index install that must fail.
- An explicit --wheel-dir that is incomplete or unverified is called out, not
  silently swapped for PyPI.

Verified end to end: built wheel and sdist both contain the wheels; a venv
with no rdm6300 self-provisions with a dead proxy and no PIP_NO_INDEX; and a
tampered wheel is refused with nothing installed.
This commit is contained in:
2026-10-01 15:48:34 +03:00
parent eaf3f394fc
commit 23cd8f2df5
9 changed files with 413 additions and 21 deletions
+29 -15
View File
@@ -89,7 +89,7 @@ Also works as a module: `python -m rdm_card_reader`.
| `--demo` | – | Simulated reads; needs no hardware and no `rdm6300` |
| `--demo-count` | `3` | Number of simulated reads in `--demo` |
| `--no-install-deps` | off | Never install missing libraries automatically |
| `--wheel-dir` | – | Install missing libraries from local wheels (offline) |
| `--wheel-dir` | bundled | Install from local wheels in this DIR instead of the bundled ones |
| `--version` | – | Print the version |
With `--serve`: `--host` (`127.0.0.1`), `--bridge-port` (`8765`), `--mode`
@@ -115,32 +115,46 @@ is what you want when piping into another program:
### Automatic dependency install
On first start the reader checks that it can import its libraries and installs
any that are missing, so a freshly flashed device works without manual setup:
The wheels for `rdm6300` and `pyserial` **ship inside this package**
(`rdm_card_reader/wheels/`), so a device with no internet can still set itself
up. On first start the reader installs whatever is missing from those wheels:
```
Reader library not found - checking dependencies...
Missing reader dependencies: rdm6300
Installing from PyPI ...
Using the wheels bundled with this package:
/opt/rdm-card-reader/.venv/lib/python3.13/site-packages/rdm_card_reader/wheels
Installing from .../rdm_card_reader/wheels
Dependencies ready: rdm6300
```
No network access is needed. They ship with the package rather than being
fetched at build time, because the build machine and the target device are not
necessarily the same.
Both wheels are **pure Python** (`py3-none-any` / `py2.py3-none-any`), which is
what makes this possible: the same files install on armv7l, aarch64 and x86_64.
This differs from the workstation app's `Files/repository/` cache, which held
architecture-specific binaries (aarch64 manylinux wheels) and so could not be
committed — that cache only ever existed on the Pi devices themselves.
* Nothing is installed when the libraries are already present, so a normal start
does no network work at all.
* A failed install does not crash: it reports `exit 2` and tells you the exact
command to run by hand.
* The bundled install runs with `--no-index`, so it works with no route to PyPI
at all — no proxy or DNS needed.
* Wheels are verified against a bundled `SHA256SUMS` before installation. **A
wheel that fails its checksum is refused, not installed** — then it falls back
to PyPI, and if that is unreachable it reports `exit 2` rather than proceeding
with something unverified.
* Outside a virtualenv pip is given `--break-system-packages`, which Debian and
Raspberry Pi OS require before they will touch the system Python.
* A failed install does not crash: it reports `exit 2` and tells you the exact
command to run by hand.
For a device with **no internet**, put the wheels next to the app and install
from there — no index is contacted:
```bash
pip download rdm6300 pyserial -d wheels # on a machine with internet
rdm-card-reader --serve --wheel-dir ./wheels
```
To manage dependencies yourself, pass `--no-install-deps`.
To install from your own wheel directory instead (for example a newer version
than the bundled one), pass `--wheel-dir`. An explicit directory that is
incomplete or fails verification is reported and then PyPI is used, rather than
failing outright. To manage dependencies yourself, pass `--no-install-deps`.
### Exit codes