feat(install): support Raspberry Pi OS Bookworm (Python 3.11) alongside Trixie (3.13) (#689)

The installer and scripts/check_system_compatibility.sh share one set of OS rules (scripts/install/lib_os.sh): Bookworm (Debian 12, Python 3.11) and Trixie (Debian 13, Python 3.13) are supported, python3 older than 3.11 stops the install before anything changes, and dhcpcd gets a warning with directions. setcap targets /usr/bin/python3, the apt fallback honours the requirement floors, and the desktop check no longer misreads under pipefail. CI runs the unit and plugin-safety suites on 3.11 and 3.13 (tooling jobs on 3.13); mypy targets 3.11.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-10-03 13:35:13 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent f841fa36b6
commit 05deb1ee7d
17 changed files with 812 additions and 111 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: "3.12"
python-version: "3.13"
# No dependencies: the script reads src/__init__.py and CHANGELOG.md only.
- name: Assert the tag, CHANGELOG and src.__version__ agree
+19 -8
View File
@@ -14,8 +14,14 @@ permissions:
jobs:
plugin-safety:
name: Plugin safety harness + unit tests
name: Plugin safety harness + unit tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
# The two Pythons the installer supports: Raspberry Pi OS Bookworm ships
# 3.11 and Trixie 3.13.
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.13"]
env:
# The bundled fixture plugin gives the harness at least one real plugin
# to render, and REQUIRE_PLUGINS turns "discovered zero plugins" into a
@@ -29,7 +35,7 @@ jobs:
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: "3.12"
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install dependencies
@@ -43,8 +49,13 @@ jobs:
pytest --no-cov test/plugins/
unit-tests:
name: Core unit tests
name: Core unit tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
# Bookworm's Python (3.11) and Trixie's (3.13); see plugin-safety.
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.13"]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
@@ -52,7 +63,7 @@ jobs:
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: "3.12"
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install dependencies
@@ -84,7 +95,7 @@ jobs:
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: "3.12"
python-version: "3.13"
cache: pip
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
@@ -123,7 +134,7 @@ jobs:
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: "3.12"
python-version: "3.13"
# Downloads the pinned standalone Tailwind CLI (SHA-256 checked; no
# Node), rebuilds static/v3/tailwind.css and plugin-frame.css from the
@@ -142,7 +153,7 @@ jobs:
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: "3.12"
python-version: "3.13"
cache: pip
# The runtime requirements are installed so mypy sees the real types of
@@ -181,7 +192,7 @@ jobs:
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: "3.12"
python-version: "3.13"
# Stdlib only; exits 0 whatever it finds.
- name: Report method-family drift across the nine scoreboards
+28
View File
@@ -19,6 +19,34 @@ accepts both, but the store flags the old spelling as deprecated
## Unreleased
### Install
- Raspberry Pi OS **Bookworm** (Debian 12, Python 3.11) is supported,
alongside **Trixie** (Debian 13, Python 3.13). The installer used to stop
on anything but Trixie. Which releases and Pythons are accepted now lives
in one place, `scripts/install/lib_os.sh`, which `first_time_install.sh`
and `scripts/check_system_compatibility.sh` both read, so the two can no
longer disagree (the compatibility check called Bookworm an error, and
still accepted Python 3.10, which the rgbmatrix bindings refuse). An
unsupported system gets plain directions to the right image; a `python3`
older than 3.11 stops the install before anything changes.
- The installer says up front when the Pi runs dhcpcd instead of
NetworkManager, and how to switch back: the web page's WiFi tab and the
`LEDMatrix-Setup` hotspot need NetworkManager. Not fatal, and it does not
switch the network stack itself, since that can cut the SSH session.
- The desktop check no longer misses a desktop install: `dpkg -l | grep -q`
under `pipefail` read a match as "not found".
- `cap_sys_nice` is set on the interpreter the services run
(`/usr/bin/python3`); it preferred `/usr/bin/python3.13` whenever it
existed.
- The Step 7 dependency fallback (`scripts/install_dependencies_apt.py`) no
longer accepts apt packages older than the pins -- Bookworm's Flask 2.2.2
and Pillow 9.4, Trixie's Flask 3.1.1 and Pillow 11.1. The floors are read
from `web_interface/requirements.txt`, and pip is asked for `Pillow`, not
`PIL`.
- CI runs the unit and plugin-safety suites on Python 3.11 and 3.13 (was
3.12); mypy targets 3.11.
### Updates refresh the systemd units; new installs run the newest release
- **Updates now install changed systemd units.** An update (Update Code, or
+6 -1
View File
@@ -151,6 +151,11 @@ The system supports live, recent, and upcoming game information for multiple spo
- **1GB models (Pi 3B / 3B+), the 512MB Pi Zero 2 W and other low-memory boards**: supported, but the `rpi-rgb-led-matrix` C++ build needs more memory than the Pi has. The installer detects this automatically, compiles with fewer parallel jobs, and adds a temporary swapfile for the build which it removes afterwards. Expect that step to take 15-25 minutes instead of 2-5, and leave at least **3GB free** on the SD card. If you manage swap yourself, opt out with `--skip-swap`. To pin the compiler down further, use `--build-jobs 1`. Once running, keep an eye on memory: see [docs/LOW_MEMORY_BOARDS.md](docs/LOW_MEMORY_BOARDS.md).
### Operating system
- **Raspberry Pi OS Lite, Trixie (Debian 13) or Bookworm (Debian 12)**, 64-bit recommended. Trixie is the current release and the one to pick for a new SD card; an existing Bookworm install works as it is, no upgrade needed. The installer checks this first and stops with directions on anything else (Bullseye and older, the desktop edition, other distributions).
- **Python**: whatever the OS ships, 3.13 on Trixie and 3.11 on Bookworm. Don't install a different Python; the installer and the services use the system `python3`.
- **Networking**: NetworkManager, the default on both. Choosing a WiFi network from the web page and the `LEDMatrix-Setup` hotspot need it; if you switched to dhcpcd in `raspi-config`, switch back (Advanced Options → Network Config → NetworkManager).
### RGB Matrix Bonnet / HAT
- [Adafruit RGB Matrix Bonnet/HAT](https://www.adafruit.com/product/3211) – supports one “chain” of horizontally connected displays
- [Adafruit Triple LED Matrix Bonnet](https://www.adafruit.com/product/6358) – supports up to 3 vertical “chains” of horizontally connected displays *(use `regular` as hardware mapping)*
@@ -249,7 +254,7 @@ These are not required and you can probably rig up something basic with stuff yo
<img width="512" height="361" alt="Step 2 Other " src="https://github.com/user-attachments/assets/166a22e8-8067-48df-9f80-50c91f573356" />
5. Then choose Raspbian OS (64-bit) Lite (Trixie)
5. Then choose Raspbian OS (64-bit) Lite (Trixie). Bookworm Lite (listed as Legacy) also works; see [Operating system](#operating-system) below
<img width="512" height="361" alt="Step 4 Trixie Lite 64" src="https://github.com/user-attachments/assets/3b8590ce-b810-4dfe-9253-26e0d4f8ed1e" />
+2 -2
View File
@@ -17,13 +17,13 @@ The LEDMatrix emulator allows you to run and test LEDMatrix displays on your com
## Prerequisites
### System Requirements
- Python 3.10 or higher
- Python 3.11 or higher (3.11 and 3.13 are tested)
- Windows, macOS, or Linux
- At least 2GB RAM (4GB recommended)
- Internet connection for plugin downloads
### Required Software
- Python 3.10+
- Python 3.11+
- pip (Python package manager)
- Git (for plugin management)
+8 -1
View File
@@ -15,6 +15,12 @@ This guide will help you set up your LEDMatrix display for the first time and ge
- Power supply (5V, 4A minimum recommended)
- MicroSD card (16GB minimum)
**Software:**
- Raspberry Pi OS Lite, Trixie (Debian 13) or Bookworm (Debian 12). Trixie
is the current release; Bookworm is listed as Legacy in Raspberry Pi
Imager. No other system is supported, and the installer says so up front.
- The OS's own Python: 3.13 on Trixie, 3.11 on Bookworm
**Network:**
- WiFi network (or Ethernet cable)
- Computer with web browser on same network
@@ -28,7 +34,8 @@ This guide will help you set up your LEDMatrix display for the first time and ge
There is no prebuilt SD card image — you install LEDMatrix onto stock
Raspberry Pi OS Lite yourself:
1. Flash Raspberry Pi OS Lite to the MicroSD card (Raspberry Pi Imager)
1. Flash Raspberry Pi OS Lite (Trixie, or Bookworm) to the MicroSD card
(Raspberry Pi Imager)
2. Connect the LED matrix to your Raspberry Pi, insert the card, and
power on
3. SSH into the Pi and run the one-shot installer:
+44
View File
@@ -84,6 +84,43 @@ python3 web_interface/start.py
### Installation & Build Issues
#### "This version of Raspberry Pi OS is not supported"
LEDMatrix installs on Raspberry Pi OS Lite **Trixie** (Debian 13, Python
3.13) or **Bookworm** (Debian 12, Python 3.11). The installer checks
`/etc/os-release` before it changes anything and stops on anything else.
**Check what you have:**
```bash
grep -E '^(PRETTY_NAME|VERSION_ID)=' /etc/os-release
python3 --version
```
**Solutions:**
- `VERSION_ID="11"` (Bullseye) or older: flash a new card with Raspberry Pi
Imager, choosing Raspberry Pi OS Lite (64-bit). Trixie is recommended;
Bookworm (Legacy) also works. An in-place upgrade from Bullseye is not
supported by Raspberry Pi and is not worth the risk.
- "Desktop environment detected": use the Lite image, not the desktop one.
- "python3 is Python 3.x; LEDMatrix needs Python 3.11 or newer": something
has replaced the system `python3`. Point it back at the OS's own Python
(`/usr/bin/python3` should be 3.11 on Bookworm, 3.13 on Trixie).
- `sudo bash scripts/check_system_compatibility.sh` runs the same checks
without installing anything.
#### "This Pi manages its network with dhcpcd, not NetworkManager"
A warning, not an error: the install carries on and the display works. But
choosing a WiFi network from the web page and the `LEDMatrix-Setup` hotspot
both need NetworkManager, the default on Bookworm and Trixie. It appears
when dhcpcd was selected in `raspi-config`. Switch back with a keyboard and
screen attached (or over Ethernet), since the WiFi connection drops briefly:
```bash
sudo raspi-config # Advanced Options -> Network Config -> NetworkManager
sudo reboot
```
#### Step 6 fails: "Failed building wheel for rgbmatrix"
**Symptoms:**
@@ -407,9 +444,16 @@ then restore only that file, for example
5. **Check required services:**
```bash
systemctl is-active NetworkManager # must say "active"
sudo systemctl status hostapd
sudo systemctl status dnsmasq
```
On a fresh install `hostapd` shows as **masked**. That is expected, on
Bookworm and Trixie alike: Debian's hostapd package masks the service
when it is installed without a configuration, so the hotspot is brought
up through NetworkManager instead (look for `nmcli hotspot fallback` in
`journalctl -u ledmatrix-wifi-monitor`). If NetworkManager is not
active, see "This Pi manages its network with dhcpcd" above.
6. **Manually enable AP mode:**
```bash
+76 -33
View File
@@ -47,38 +47,51 @@ if echo "${DEVICE_MODEL:-}" | grep -qi "Raspberry Pi 5"; then
echo "Raspberry Pi 5 detected — will verify RP1 library support."
fi
# Check OS version - must be Raspberry Pi OS Lite (Trixie)
# Check OS version - must be Raspberry Pi OS Lite, Bookworm or Trixie.
# The rules live in scripts/install/lib_os.sh, shared with
# scripts/check_system_compatibility.sh.
echo ""
echo "Checking operating system requirements..."
echo "----------------------------------------"
OS_CHECK_FAILED=0
OS_RELEASE=""
if [ -f /etc/os-release ]; then
. /etc/os-release
echo "Detected OS: $PRETTY_NAME"
echo "Version ID: ${VERSION_ID:-unknown}"
OS_LIB="$(cd "$(dirname "$0")" && pwd)/scripts/install/lib_os.sh"
if [ ! -f "$OS_LIB" ]; then
echo "✗ ERROR: $OS_LIB is missing, so the operating system cannot be checked."
echo " Your LEDMatrix download is incomplete. Download it again and re-run this script:"
echo " git clone https://github.com/ChuckBuilds/LEDMatrix.git"
exit 1
fi
# shellcheck source=scripts/install/lib_os.sh
. "$OS_LIB"
# Check if it's Raspberry Pi OS or Debian
if [[ "$ID" != "raspbian" ]] && [[ "$ID" != "debian" ]]; then
echo "✗ ERROR: This script requires Raspberry Pi OS (raspbian/debian)"
echo " Detected OS ID: $ID"
OS_CHECK_FAILED=1
fi
if [ -r "$LM_OS_RELEASE_FILE" ]; then
echo "Detected OS: $(lm_os_field PRETTY_NAME)"
OS_VERSION_ID=$(lm_os_field VERSION_ID)
echo "Version ID: ${OS_VERSION_ID:-unknown}"
# Check if it's Debian 13 (Trixie)
if [ "${VERSION_ID:-0}" != "13" ]; then
echo "✗ ERROR: This script requires Raspberry Pi OS Lite (Trixie) - Debian 13"
echo " Detected version: ${VERSION_ID:-unknown}"
echo " Please upgrade to Raspberry Pi OS Lite (Trixie) before continuing"
OS_CHECK_FAILED=1
if OS_RELEASE=$(lm_os_release); then
echo "✓ $(lm_release_label "$OS_RELEASE") detected"
else
echo "✓ Debian 13 (Trixie) detected"
OS_ID=$(lm_os_field ID)
if [[ "$OS_ID" != "raspbian" ]] && [[ "$OS_ID" != "debian" ]]; then
echo "✗ ERROR: This script requires Raspberry Pi OS (raspbian/debian)"
echo " Detected OS ID: ${OS_ID:-unknown}"
else
echo "✗ ERROR: This version of Raspberry Pi OS is not supported"
echo " Detected version: ${OS_VERSION_ID:-unknown}"
echo " Supported: Trixie (Debian 13) and Bookworm (Debian 12)"
fi
OS_CHECK_FAILED=1
fi
# Check if it's the Lite version (no desktop environment)
# Check for desktop packages or desktop services
DESKTOP_DETECTED=0
if dpkg -l | grep -qE "^ii.*raspberrypi-ui-mods|^ii.*lxde|^ii.*xfce|^ii.*gnome|^ii.*kde"; then
# grep without -q: -q exits at the first match, dpkg then dies of SIGPIPE,
# and pipefail turns a found desktop into "not found".
if dpkg -l | grep -E "^ii.*raspberrypi-ui-mods|^ii.*lxde|^ii.*xfce|^ii.*gnome|^ii.*kde" >/dev/null; then
DESKTOP_DETECTED=1
fi
if systemctl list-units --type=service --state=running 2>/dev/null | grep -qE "lightdm|gdm3|sddm|lxdm"; then
@@ -96,23 +109,52 @@ if [ -f /etc/os-release ]; then
echo "✓ Lite version confirmed (no desktop environment)"
fi
else
echo "✗ ERROR: Could not detect OS version (/etc/os-release not found)"
echo "✗ ERROR: Could not detect OS version ($LM_OS_RELEASE_FILE not found)"
OS_CHECK_FAILED=1
fi
# Python: whatever python3 the release ships (3.11 on Bookworm, 3.13 on
# Trixie). Checked only when python3 is already there -- Step 1 installs it
# otherwise, and on a supported release that brings the release's own version.
if [ "$OS_CHECK_FAILED" -eq 0 ]; then
if PYTHON3_VERSION=$(lm_python_version); then
case "$(lm_python_check "$PYTHON3_VERSION")" in
ok)
echo "✓ Python $PYTHON3_VERSION detected"
;;
too-old)
echo "✗ ERROR: python3 is Python $PYTHON3_VERSION; LEDMatrix needs Python 3.$LM_PYTHON_MIN_MINOR or newer"
echo " $(lm_release_label "$OS_RELEASE") ships Python $(lm_release_python "$OS_RELEASE"). Something on this"
echo " system has changed which Python 'python3' runs; point it back at the system Python."
OS_CHECK_FAILED=1
;;
*)
echo "⚠ python3 is Python $PYTHON3_VERSION, which LEDMatrix has not been tested with"
echo " (tested: 3.$LM_PYTHON_MIN_MINOR to 3.$LM_PYTHON_MAX_MINOR). Continuing anyway."
;;
esac
else
echo "python3 not found yet; Step 1 installs it."
fi
fi
if [ "$OS_CHECK_FAILED" -eq 1 ]; then
echo ""
echo "Installation cannot continue. Please install Raspberry Pi OS Lite (Trixie) and try again."
echo ""
echo "To install Raspberry Pi OS Lite (Trixie):"
echo " 1. Download from: https://www.raspberrypi.com/software/operating-systems/"
echo " 2. Select 'Raspberry Pi OS Lite (64-bit)' with Debian 13 (Trixie)"
echo " 3. Flash to SD card using Raspberry Pi Imager"
echo " 4. Boot and run this script again"
echo "Installation cannot continue."
lm_print_supported_os_help
exit 1
fi
echo "✓ OS requirements met"
# WiFi setup (the web page's WiFi tab and the LEDMatrix-Setup hotspot) needs
# NetworkManager. Both releases use it by default; say so plainly if this Pi
# does not, but carry on -- the display itself does not depend on it.
case "$(lm_network_stack)" in
networkmanager) echo "✓ NetworkManager is managing the network" ;;
dhcpcd) lm_print_dhcpcd_advice ;;
*) echo "⚠ Could not tell which service manages the network; WiFi setup from the web page needs NetworkManager" ;;
esac
echo ""
# The user who ran the installer: SUDO_USER once we are running under sudo
@@ -1370,15 +1412,16 @@ if ! command -v setcap >/dev/null 2>&1; then
echo "⚠ setcap not found, skipping capability configuration"
echo " Install libcap2-bin if you need hardware timing capabilities"
else
# Find the Python binary and resolve symlinks to get the real binary
# The binary the services run (ExecStart=/usr/bin/python3), symlinks
# resolved: python3.11 on Bookworm, python3.13 on Trixie. This used to
# prefer /usr/bin/python3.13 whenever it existed, which would set the
# capability on an interpreter the services never run if python3 pointed
# elsewhere.
PYTHON_BIN=""
PYTHON_VER=""
if [ -f "/usr/bin/python3.13" ]; then
PYTHON_BIN=$(readlink -f /usr/bin/python3.13)
PYTHON_VER="3.13"
elif [ -f "/usr/bin/python3" ]; then
if [ -f "/usr/bin/python3" ]; then
PYTHON_BIN=$(readlink -f /usr/bin/python3)
PYTHON_VER=$(python3 --version 2>&1 | grep -oP '(?<=Python )\d+\.\d+' || echo "unknown")
PYTHON_VER=$(lm_python_version /usr/bin/python3) || PYTHON_VER="unknown"
fi
if [ -n "$PYTHON_BIN" ] && [ -f "$PYTHON_BIN" ]; then
+5 -4
View File
@@ -6,8 +6,9 @@
files = src
exclude = (^|/)(test|__pycache__)/
# Python version
python_version = 3.10
# Python version: the oldest the installer supports (Raspberry Pi OS
# Bookworm ships 3.11; Trixie ships 3.13).
python_version = 3.11
# Platform (Linux/Raspberry Pi)
platform = linux
@@ -103,8 +104,8 @@ ignore_missing_imports = True
# numpy's own stubs (numpy>=2.3) use Python 3.12 `type` statements, which mypy
# refuses to parse under python_version = 3.10 -- and 3.10 is the floor this
# code has to run on, so it stays. Treat numpy as Any instead: skip it, and
# refuses to parse under python_version = 3.11 -- and 3.11 (Bookworm) is the
# floor this code has to run on, so it stays. Treat numpy as Any instead: skip it, and
# follow_imports_for_stubs makes the skip apply to its .pyi files too.
[mypy-numpy.*]
follow_imports = skip
+1 -1
View File
@@ -1,5 +1,5 @@
# LEDMatrix Core Dependencies
# Compatible with Python 3.10, 3.11, 3.12, and 3.13
# Compatible with Python 3.11, 3.12 and 3.13; CI tests 3.11 and 3.13
# Tested on Raspbian OS 12 (Bookworm) and 13 (Trixie)
# Image processing
+66 -32
View File
@@ -53,26 +53,35 @@ else
fi
echo ""
# Check OS version
# Check OS version. The supported releases come from the same library the
# installer uses, so the two cannot disagree.
echo "2. Checking Operating System Version..."
echo "---------------------------------------"
if [ -f /etc/os-release ]; then
. /etc/os-release
echo "OS: $PRETTY_NAME"
echo "Version ID: ${VERSION_ID:-unknown}"
OS_LIB="$(cd "$(dirname "$0")" && pwd)/install/lib_os.sh"
OS_LIB_LOADED=0
OS_RELEASE=""
if [ -f "$OS_LIB" ]; then
# shellcheck source=scripts/install/lib_os.sh
. "$OS_LIB"
OS_LIB_LOADED=1
fi
# first_time_install.sh refuses anything but Raspberry Pi OS / Debian 13
# (Trixie), so anything else is an error here too, not a warning.
if [[ "$ID" == "raspbian" ]] || [[ "$ID" == "debian" ]]; then
if [ "${VERSION_ID:-0}" = "13" ]; then
print_success "Detected Debian 13 Trixie - supported"
elif [ "${VERSION_ID:-0}" = "12" ]; then
print_error "Debian 12 Bookworm is not supported - the installer requires Raspberry Pi OS Lite (Trixie), Debian 13"
else
print_error "Debian/Raspbian ${VERSION_ID:-unknown} is not supported - the installer requires Raspberry Pi OS Lite (Trixie), Debian 13"
fi
if [ "$OS_LIB_LOADED" = "0" ]; then
print_error "$OS_LIB is missing - download LEDMatrix again"
elif [ -r "$LM_OS_RELEASE_FILE" ]; then
OS_ID=$(lm_os_field ID)
OS_VERSION_ID=$(lm_os_field VERSION_ID)
echo "OS: $(lm_os_field PRETTY_NAME)"
echo "Version ID: ${OS_VERSION_ID:-unknown}"
# first_time_install.sh refuses anything else, so this is an error here
# too, not a warning.
if OS_RELEASE=$(lm_os_release); then
print_success "Detected $(lm_release_label "$OS_RELEASE") - supported"
elif [[ "$OS_ID" == "raspbian" ]] || [[ "$OS_ID" == "debian" ]]; then
print_error "Debian/Raspbian ${OS_VERSION_ID:-unknown} is not supported - the installer requires Raspberry Pi OS Lite, Trixie (Debian 13) or Bookworm (Debian 12)"
else
print_error "${ID:-unknown} is not supported - the installer requires Raspberry Pi OS Lite (Trixie), Debian 13"
print_error "${OS_ID:-unknown} is not supported - the installer requires Raspberry Pi OS Lite, Trixie (Debian 13) or Bookworm (Debian 12)"
fi
else
print_error "Could not detect OS version"
@@ -92,7 +101,7 @@ if [ "$KERNEL_MAJOR" -ge "6" ]; then
print_success "Kernel version is compatible (6.x or newer)"
if [ "$KERNEL_MAJOR" -eq "6" ] && [ "$KERNEL_MINOR" -ge "12" ]; then
print_success "Running latest Trixie kernel (6.12 LTS)"
print_success "Running a 6.12 LTS or newer kernel"
fi
elif [ "$KERNEL_MAJOR" -eq "5" ] && [ "$KERNEL_MINOR" -ge "10" ]; then
print_success "Kernel version is compatible (5.10+)"
@@ -104,25 +113,34 @@ echo ""
# Check Python version
echo "4. Checking Python Version..."
echo "-----------------------------"
if command -v python3 >/dev/null 2>&1; then
PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}")')
PYTHON_MAJOR=$(python3 -c 'import sys; print(sys.version_info.major)')
PYTHON_MINOR=$(python3 -c 'import sys; print(sys.version_info.minor)')
if [ "$OS_LIB_LOADED" = "1" ] && command -v python3 >/dev/null 2>&1; then
PYTHON_VERSION=$(python3 -c 'import sys; print("%d.%d.%d" % sys.version_info[:3])')
PYTHON_MINOR_VERSION=$(lm_python_version) || PYTHON_MINOR_VERSION=""
PYTHON_RANGE="3.${LM_PYTHON_MIN_MINOR}-3.${LM_PYTHON_MAX_MINOR}"
echo "Python: $PYTHON_VERSION"
if [ "$PYTHON_MAJOR" -eq "3" ]; then
if [ "$PYTHON_MINOR" -ge "10" ] && [ "$PYTHON_MINOR" -le "13" ]; then
print_success "Python version is supported (3.10-3.13)"
elif [ "$PYTHON_MINOR" -ge "14" ]; then
print_warning "Python 3.${PYTHON_MINOR} is very new - some packages may not be compatible yet"
else
# Pillow 12 and the pinned test tools need 3.10+, so this won't install.
print_error "Python 3.${PYTHON_MINOR} is too old - Python 3.10+ is required"
fi
else
print_error "Python 2.x detected - Python 3.10+ is required"
case "$(lm_python_check "$PYTHON_MINOR_VERSION")" in
ok)
print_success "Python version is supported ($PYTHON_RANGE)"
;;
too-old)
# The rgbmatrix bindings declare requires-python >=3.11, so the
# display cannot be built on anything older.
print_error "Python $PYTHON_MINOR_VERSION is too old - Python 3.${LM_PYTHON_MIN_MINOR}+ is required"
;;
too-new)
print_warning "Python $PYTHON_MINOR_VERSION is newer than LEDMatrix has been tested with ($PYTHON_RANGE)"
;;
*)
print_warning "Could not read the Python version"
;;
esac
if [ -n "$OS_RELEASE" ] && [ "$PYTHON_MINOR_VERSION" != "$(lm_release_python "$OS_RELEASE")" ]; then
print_warning "$(lm_release_label "$OS_RELEASE") ships Python $(lm_release_python "$OS_RELEASE"), but python3 runs $PYTHON_MINOR_VERSION"
fi
elif command -v python3 >/dev/null 2>&1; then
print_warning "Cannot check the Python version without $OS_LIB"
else
print_error "Python 3 not found - installation required"
fi
@@ -268,6 +286,22 @@ if command -v ping >/dev/null 2>&1; then
else
print_warning "Ping command not available - cannot verify network"
fi
# WiFi setup from the web page and the LEDMatrix-Setup hotspot drive
# NetworkManager, the default on both Bookworm and Trixie.
if [ "$OS_LIB_LOADED" = "1" ]; then
case "$(lm_network_stack)" in
networkmanager)
print_success "NetworkManager manages the network (needed for WiFi setup)"
;;
dhcpcd)
print_warning "dhcpcd manages the network - WiFi setup from the web page and the setup hotspot need NetworkManager (sudo raspi-config -> Advanced Options -> Network Config)"
;;
*)
print_warning "Could not tell which service manages the network - WiFi setup from the web page needs NetworkManager"
;;
esac
fi
echo ""
# Print summary
+3
View File
@@ -41,6 +41,9 @@ Libraries (sourced, not run):
script that renders a unit from `systemd/*.service`
- **`lib_lowmem.sh`** - Build-job sizing and temporary swap for the C++
build on low-memory Pis (`first_time_install.sh` Step 6)
- **`lib_os.sh`** - Which releases (Bookworm, Trixie) and Python versions
(3.11-3.13) the installer accepts, and which service runs the network;
shared by `first_time_install.sh` and `scripts/check_system_compatibility.sh`
## Usage
+138
View File
@@ -0,0 +1,138 @@
#!/bin/bash
# Which operating systems and Python versions LEDMatrix installs on.
#
# Sourced by first_time_install.sh and scripts/check_system_compatibility.sh,
# so the installer and the compatibility checker cannot disagree about what
# is supported. Pure functions: nothing here installs, changes or exits --
# the callers decide what to do with the answers.
#
# Supported (Lite, no desktop):
# Raspberry Pi OS / Debian 12 "Bookworm" -- Python 3.11
# Raspberry Pi OS / Debian 13 "Trixie" -- Python 3.13
#
# Everything the installer asks apt for (python3-pip, python3-venv,
# python-dev-is-python3, python3-pil, python3-pil.imagetk, build-essential,
# python3-setuptools, python3-wheel, cmake, ninja-build, git, curl, wget,
# unzip, and hostapd, dnsmasq, network-manager for WiFi setup) has the same
# name on both releases. Both ship a pip (23.0.1 and 25.1.1) that is PEP 668
# "externally managed" and accepts --break-system-packages, and a cmake (3.25
# and 3.31) new enough for the rgbmatrix build (3.22). So no step needs a
# per-release branch today; if one ever does, the release name comes from
# lm_os_release below.
# Test hook: the os-release file to read.
LM_OS_RELEASE_FILE="${LM_OS_RELEASE_FILE:-/etc/os-release}"
# Oldest and newest python3 minor versions the installer accepts. 3.11 is
# Bookworm's, and also the floor of the rgbmatrix bindings (requires-python
# >=3.11 in rpi-rgb-led-matrix-master/pyproject.toml); 3.13 is Trixie's.
LM_PYTHON_MIN_MINOR=11
LM_PYTHON_MAX_MINOR=13
# lm_os_field KEY -- one value from os-release with its quotes removed; empty
# when the key or the file is missing. Parsed rather than sourced so that
# os-release's ID, VERSION and friends do not land in the caller's variables.
lm_os_field() {
[ -r "$LM_OS_RELEASE_FILE" ] || return 0
sed -n "/^$1=/{s/^$1=//;s/^[\"']//;s/[\"']\$//;p;q;}" "$LM_OS_RELEASE_FILE"
}
# lm_os_release -- print "bookworm" or "trixie" and succeed on a supported
# release; print nothing and fail on anything else. VERSION_ID decides; the
# codename is used only when VERSION_ID is missing.
lm_os_release() {
local id version
id=$(lm_os_field ID)
version=$(lm_os_field VERSION_ID)
[ -n "$version" ] || version=$(lm_os_field VERSION_CODENAME)
case "$id" in
raspbian|debian) ;;
*) return 1 ;;
esac
case "$version" in
12|bookworm) echo bookworm ;;
13|trixie) echo trixie ;;
*) return 1 ;;
esac
}
# lm_release_label RELEASE -- how to name a release to a person.
lm_release_label() {
case "$1" in
bookworm) echo "Debian 12 (Bookworm)" ;;
trixie) echo "Debian 13 (Trixie)" ;;
*) echo "$1" ;;
esac
}
# lm_release_python RELEASE -- the python3 version a release ships, e.g. 3.11.
lm_release_python() {
case "$1" in
bookworm) echo 3.11 ;;
trixie) echo 3.13 ;;
*) return 1 ;;
esac
}
# lm_python_version [PYTHON] -- "3.11" and so on for python3 (or PYTHON);
# prints nothing and fails when it cannot be run.
lm_python_version() {
"${1:-python3}" -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null
}
# lm_python_check VERSION -- print "ok", "too-old", "too-new" or "unknown"
# for a version such as 3.11. Always succeeds, so it is safe under set -e.
lm_python_check() {
local major minor
major=${1%%.*}
minor=${1#*.}
minor=${minor%%.*}
case "$major:$minor" in
*[!0-9:]*|:*|*:) echo unknown; return 0 ;;
esac
if [ "$major" -lt 3 ] || { [ "$major" -eq 3 ] && [ "$minor" -lt "$LM_PYTHON_MIN_MINOR" ]; }; then
echo too-old
elif [ "$major" -gt 3 ] || [ "$minor" -gt "$LM_PYTHON_MAX_MINOR" ]; then
echo too-new
else
echo ok
fi
}
# lm_network_stack -- which service runs the network: "networkmanager",
# "dhcpcd" or "unknown". Raspberry Pi OS uses NetworkManager on both Bookworm
# and Trixie; dhcpcd appears when someone switched back to it in raspi-config.
lm_network_stack() {
if systemctl is-active --quiet NetworkManager 2>/dev/null; then
echo networkmanager
elif systemctl is-active --quiet dhcpcd 2>/dev/null; then
echo dhcpcd
else
echo unknown
fi
}
# lm_print_dhcpcd_advice -- the explanation for a Pi running dhcpcd. WiFi
# setup from the web page and the LEDMatrix-Setup hotspot both drive
# NetworkManager (nmcli). The installer does not switch the network stack
# itself: doing that over SSH can cut the connection it is running on.
lm_print_dhcpcd_advice() {
echo "⚠ This Pi manages its network with dhcpcd, not NetworkManager."
echo " LEDMatrix installs and the display works, but choosing a WiFi network"
echo " from the web page and the LEDMatrix-Setup hotspot both need NetworkManager."
echo " To switch (with a keyboard and screen attached, or over Ethernet):"
echo " sudo raspi-config -> Advanced Options -> Network Config -> NetworkManager"
echo " then reboot."
}
# lm_print_supported_os_help -- what to do on an unsupported system.
lm_print_supported_os_help() {
echo "LEDMatrix needs Raspberry Pi OS Lite: Trixie (Debian 13) or Bookworm (Debian 12)."
echo ""
echo "To install Raspberry Pi OS Lite:"
echo " 1. Download Raspberry Pi Imager from: https://www.raspberrypi.com/software/"
echo " 2. Choose 'Raspberry Pi OS Lite (64-bit)'. Trixie is the current version and"
echo " is recommended; Bookworm (listed as Legacy) also works"
echo " 3. Flash it to the SD card"
echo " 4. Boot the Pi and run this script again"
}
+63 -19
View File
@@ -4,13 +4,14 @@ Alternative dependency installer that tries apt packages first,
then falls back to pip with --break-system-packages
"""
import re
import subprocess
import sys
import tempfile
import warnings
from collections import deque
from pathlib import Path
from typing import List, Tuple
from typing import Dict, List, Tuple
# How many trailing lines of a failed command's output to keep for the
# end-of-run failure summary. Keeps the root cause near the end of the log,
@@ -81,6 +82,8 @@ def install_via_pip(package_name: str) -> Tuple[bool, str]:
Returns (success, output).
"""
# pip knows PIL as Pillow; the others are asked for by their own name.
package_name = _dist_name(package_name)
print(f"Installing {package_name} via pip...")
success, output = _run([
sys.executable, '-m', 'pip', 'install',
@@ -99,26 +102,66 @@ IMPORT_NAME_MAP = {
'freetype-py': 'freetype',
}
# Minimum versions that must be met for an already-installed package to count
# as satisfied. Debian Bookworm's python3-freetype is 2.3.0, below the
# freetype-py>=2.5.1 pin in requirements.txt, so an import-only check would
# wrongly skip the pip upgrade.
MIN_VERSIONS = {
'freetype-py': (2, 5, 1),
# The packages above are keyed by what main() lists; these are the ones whose
# pip distribution name differs from that key.
DIST_NAME_MAP = {
'PIL': 'Pillow',
}
REQUIREMENTS_FILE = Path(__file__).resolve().parent.parent / 'web_interface' / 'requirements.txt'
def _version_tuple(text: str) -> tuple:
parts = []
for part in text.split('.'):
digits = ''.join(ch for ch in part if ch.isdigit())
if not digits:
break
parts.append(int(digits))
return tuple(parts)
def _requirement_floors(path: Path = REQUIREMENTS_FILE) -> Dict[str, tuple]:
"""``>=`` floors from a requirements file, keyed by lower-cased name.
The apt copies of these packages are older than the pins on both
supported releases -- Bookworm ships Flask and Werkzeug 2.2.2, Pillow 9.4,
requests 2.28, psutil 5.9, pytz 2022.7 and freetype-py 2.3; Trixie ships
Flask 3.1.1, Werkzeug 3.1.3, Pillow 11.1 and requests 2.32 --
so a package that merely imports is not enough. Read from the file rather
than copied here so the two cannot drift.
"""
floors: Dict[str, tuple] = {}
try:
lines = path.read_text(encoding='utf-8').splitlines()
except OSError:
return floors
for line in lines:
match = re.match(r'\s*([A-Za-z0-9][A-Za-z0-9._-]*)[^#]*?>=\s*([0-9][0-9.]*)', line)
if match:
floors[match.group(1).lower()] = _version_tuple(match.group(2))
return floors
def _dist_name(package_name: str) -> str:
return DIST_NAME_MAP.get(package_name, package_name)
def _minimum_version(package_name: str) -> tuple:
"""The required floor for ``package_name``, or () when there is none."""
return MIN_VERSIONS.get(_dist_name(package_name).lower(), ())
# Minimum versions that must be met for an already-installed package to count
# as satisfied.
MIN_VERSIONS = _requirement_floors()
def _installed_version_tuple(dist_name: str) -> tuple:
"""Return the installed distribution version as an int tuple, or () if unknown."""
try:
from importlib.metadata import version
parts = []
for part in version(dist_name).split('.'):
digits = ''.join(ch for ch in part if ch.isdigit())
if not digits:
break
parts.append(int(digits))
return tuple(parts)
return _version_tuple(version(dist_name))
except Exception:
return ()
@@ -134,9 +177,9 @@ def check_package_installed(package_name: str) -> bool:
__import__(import_name)
except ImportError:
return False
minimum = MIN_VERSIONS.get(package_name)
minimum = _minimum_version(package_name)
if minimum:
installed = _installed_version_tuple(package_name)
installed = _installed_version_tuple(_dist_name(package_name))
if not installed or installed < minimum:
print(f"{package_name} is installed but below the required "
f"{'.'.join(map(str, minimum))}; will upgrade via pip")
@@ -188,10 +231,11 @@ def main():
continue
# Try apt first, then pip. An apt install only counts if it also
# satisfies any minimum version (Debian's python3-freetype can be
# older than the freetype-py pin), otherwise fall through to pip.
# satisfies the requirements floor (the apt copies of most of these
# are older than the pins on both Bookworm and Trixie), otherwise
# fall through to pip.
ok, apt_output = install_via_apt(package)
if ok and package in MIN_VERSIONS and not check_package_installed(package):
if ok and _minimum_version(package) and not check_package_installed(package):
ok = False
apt_output = f"apt version of {package} is below the required minimum"
if not ok:
+343
View File
@@ -0,0 +1,343 @@
"""The installer accepts Raspberry Pi OS Bookworm and Trixie, and nothing else.
Bookworm (Debian 12) ships Python 3.11 and Trixie (Debian 13) Python 3.13.
The rules live in scripts/install/lib_os.sh, which first_time_install.sh and
scripts/check_system_compatibility.sh both source. These tests feed the real
scripts a fake /etc/os-release (LM_OS_RELEASE_FILE) and stub python3,
systemctl and dpkg, so they need a Linux bash; the installer itself cannot
run end to end off a Pi.
"""
import importlib.util
import re
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
LIB = ROOT / "scripts" / "install" / "lib_os.sh"
FIRST_TIME = ROOT / "first_time_install.sh"
COMPAT = ROOT / "scripts" / "check_system_compatibility.sh"
needs_bash = pytest.mark.skipif(
sys.platform == "win32" or shutil.which("bash") is None,
reason="runs the installer's shell code; needs a Linux bash",
)
OS_RELEASES = {
# Raspberry Pi OS 64-bit reports ID=debian, 32-bit ID=raspbian.
"trixie": 'PRETTY_NAME="Debian GNU/Linux 13 (trixie)"\nNAME="Debian GNU/Linux"\n'
'VERSION_ID="13"\nVERSION="13 (trixie)"\nVERSION_CODENAME=trixie\nID=debian\n',
"bookworm": 'PRETTY_NAME="Raspbian GNU/Linux 12 (bookworm)"\nNAME="Raspbian GNU/Linux"\n'
'VERSION_ID="12"\nVERSION="12 (bookworm)"\nVERSION_CODENAME=bookworm\nID=raspbian\n'
'ID_LIKE=debian\n',
"bookworm64": 'PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"\nVERSION_ID="12"\n'
"VERSION_CODENAME=bookworm\nID=debian\n",
"bullseye": 'PRETTY_NAME="Raspbian GNU/Linux 11 (bullseye)"\nVERSION_ID="11"\n'
"VERSION_CODENAME=bullseye\nID=raspbian\n",
"ubuntu": 'PRETTY_NAME="Ubuntu 24.04 LTS"\nVERSION_ID="24.04"\nVERSION_CODENAME=noble\n'
"ID=ubuntu\nID_LIKE=debian\n",
"no-version-id": "PRETTY_NAME='Debian GNU/Linux trixie'\nVERSION_CODENAME=trixie\nID=debian\n",
}
def _stub(bin_dir: Path, name: str, body: str) -> None:
path = bin_dir / name
path.write_text("#!/bin/sh\n" + body, encoding="utf-8", newline="\n")
path.chmod(0o755)
def _stubs(tmp_path: Path, python_version="3.11", network="NetworkManager") -> Path:
"""python3 reports ``python_version`` (None: not installed); systemctl
reports ``network`` as the only active unit; dpkg lists no desktop."""
bin_dir = tmp_path / "bin"
bin_dir.mkdir(exist_ok=True)
if python_version is None:
# Shadows any real python3 further down PATH.
_stub(bin_dir, "python3", "exit 127\n")
else:
_stub(bin_dir, "python3", f'case "$*" in *"%d.%d.%d"*) echo "{python_version}.1" ;; '
f'*) echo "{python_version}" ;; esac\n')
_stub(bin_dir, "systemctl",
f'case "$*" in *"is-active --quiet {network}") exit 0 ;; esac\nexit 3\n')
_stub(bin_dir, "dpkg", "exit 0\n")
_stub(bin_dir, "dpkg-query", "exit 1\n")
_stub(bin_dir, "ping", "exit 0\n")
return bin_dir
def _env(tmp_path: Path, release: str, bin_dir: Path) -> dict:
os_release = tmp_path / "os-release"
os_release.write_text(OS_RELEASES[release], encoding="utf-8", newline="\n")
return {
"PATH": f"{bin_dir}:/usr/bin:/bin:/usr/sbin:/sbin",
"LM_OS_RELEASE_FILE": str(os_release),
"HOME": str(tmp_path),
}
def lib(snippet: str, env: dict) -> subprocess.CompletedProcess:
return subprocess.run(["bash", "-c", f"set -Eeuo pipefail\n. '{LIB}'\n{snippet}"],
capture_output=True, text=True, env=env)
# --- lib_os.sh -----------------------------------------------------------------
@needs_bash
class TestLibrary:
def test_library_is_syntactically_valid(self):
result = subprocess.run(["bash", "-n", str(LIB)], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
@pytest.mark.parametrize("release,expected", [
("trixie", "trixie"),
("bookworm", "bookworm"),
("bookworm64", "bookworm"),
("no-version-id", "trixie"),
])
def test_supported_releases_are_recognised(self, tmp_path, release, expected):
result = lib("lm_os_release", _env(tmp_path, release, _stubs(tmp_path)))
assert result.returncode == 0, result.stderr
assert result.stdout.strip() == expected
@pytest.mark.parametrize("release", ["bullseye", "ubuntu"])
def test_other_systems_are_refused(self, tmp_path, release):
result = lib("lm_os_release", _env(tmp_path, release, _stubs(tmp_path)))
assert result.returncode != 0
assert result.stdout.strip() == ""
def test_missing_os_release_is_refused_not_fatal_to_the_caller(self, tmp_path):
env = _env(tmp_path, "trixie", _stubs(tmp_path))
env["LM_OS_RELEASE_FILE"] = str(tmp_path / "absent")
result = lib('if lm_os_release; then echo yes; else echo no; fi', env)
assert result.returncode == 0, result.stderr
assert result.stdout.strip() == "no"
def test_quoted_fields_are_unquoted(self, tmp_path):
env = _env(tmp_path, "no-version-id", _stubs(tmp_path))
result = lib("lm_os_field PRETTY_NAME; lm_os_field VERSION_ID", env)
assert result.stdout == "Debian GNU/Linux trixie\n"
@pytest.mark.parametrize("release,python", [("bookworm", "3.11"), ("trixie", "3.13")])
def test_each_release_names_the_python_it_ships(self, tmp_path, release, python):
result = lib(f"lm_release_python {release}", _env(tmp_path, "trixie", _stubs(tmp_path)))
assert result.stdout.strip() == python
@pytest.mark.parametrize("version,verdict", [
("3.11", "ok"), ("3.12", "ok"), ("3.13", "ok"),
("3.10", "too-old"), ("3.9", "too-old"), ("2.7", "too-old"),
("3.14", "too-new"), ("4.0", "too-new"),
("", "unknown"), ("garbage", "unknown"),
])
def test_python_versions(self, tmp_path, version, verdict):
result = lib(f'lm_python_check "{version}"', _env(tmp_path, "trixie", _stubs(tmp_path)))
assert result.returncode == 0, result.stderr
assert result.stdout.strip() == verdict
@pytest.mark.parametrize("active,expected", [
("NetworkManager", "networkmanager"),
("dhcpcd", "dhcpcd"),
("nothing", "unknown"),
])
def test_network_stack(self, tmp_path, active, expected):
env = _env(tmp_path, "bookworm", _stubs(tmp_path, network=active))
assert lib("lm_network_stack", env).stdout.strip() == expected
# --- first_time_install.sh's OS check ------------------------------------------
def _os_check_section() -> str:
"""first_time_install.sh from the OS check up to the next section, with
the desktop-marker directories pointed somewhere that cannot exist."""
text = FIRST_TIME.read_text(encoding="utf-8").replace("\r\n", "\n")
start = text.index("# Check OS version")
end = text.index("# The user who ran the installer")
section = text[start:end]
for marker in ("/usr/share/raspberrypi-ui-mods", "/usr/share/xsessions"):
assert marker in section
section = section.replace(marker, "/nonexistent" + marker)
return section
def run_os_check(tmp_path: Path, release: str, **stub_args) -> subprocess.CompletedProcess:
"""Run the OS check as the installer would, from a copy of the project
layout so ``$(dirname "$0")/scripts/install/lib_os.sh`` resolves."""
project = tmp_path / "project"
(project / "scripts" / "install").mkdir(parents=True)
shutil.copy(LIB, project / "scripts" / "install" / "lib_os.sh")
script = project / "first_time_install.sh"
script.write_text("set -Eeuo pipefail\n"
"trap 'echo ERR-TRAP line $LINENO >&2; exit 99' ERR\n"
+ _os_check_section() + '\necho "SECTION-DONE"\n',
encoding="utf-8", newline="\n")
env = _env(tmp_path, release, _stubs(tmp_path, **stub_args))
return subprocess.run(["bash", str(script)], capture_output=True, text=True, env=env)
@needs_bash
class TestInstallerOsCheck:
@pytest.mark.parametrize("release,python,label", [
("bookworm", "3.11", "Debian 12 (Bookworm)"),
("bookworm64", "3.11", "Debian 12 (Bookworm)"),
("trixie", "3.13", "Debian 13 (Trixie)"),
])
def test_supported_release_passes(self, tmp_path, release, python, label):
result = run_os_check(tmp_path, release, python_version=python)
assert result.returncode == 0, result.stdout + result.stderr
assert f"✓ {label} detected" in result.stdout
assert f"✓ Python {python} detected" in result.stdout
assert "✓ OS requirements met" in result.stdout
assert "SECTION-DONE" in result.stdout
@pytest.mark.parametrize("release,reason", [
("bullseye", "This version of Raspberry Pi OS is not supported"),
("ubuntu", "This script requires Raspberry Pi OS"),
])
def test_unsupported_system_stops_with_directions(self, tmp_path, release, reason):
result = run_os_check(tmp_path, release)
assert result.returncode == 1, result.stdout + result.stderr
assert reason in result.stdout
assert "Installation cannot continue." in result.stdout
assert "Trixie (Debian 13) or Bookworm (Debian 12)" in result.stdout
assert "SECTION-DONE" not in result.stdout
def test_python_older_than_the_rgbmatrix_floor_stops(self, tmp_path):
result = run_os_check(tmp_path, "bookworm", python_version="3.10")
assert result.returncode == 1, result.stdout + result.stderr
assert "needs Python 3.11 or newer" in result.stdout
assert "ships Python 3.11" in result.stdout
def test_untested_newer_python_warns_and_continues(self, tmp_path):
result = run_os_check(tmp_path, "trixie", python_version="3.14")
assert result.returncode == 0, result.stdout + result.stderr
assert "has not been tested with" in result.stdout
def test_missing_python_is_left_to_step_1(self, tmp_path):
result = run_os_check(tmp_path, "trixie", python_version=None)
assert result.returncode == 0, result.stdout + result.stderr
assert "Step 1 installs it" in result.stdout
def test_dhcpcd_is_explained_but_not_fatal(self, tmp_path):
result = run_os_check(tmp_path, "bookworm", network="dhcpcd")
assert result.returncode == 0, result.stdout + result.stderr
assert "dhcpcd, not NetworkManager" in result.stdout
assert "Network Config" in result.stdout
def test_networkmanager_is_confirmed(self, tmp_path):
result = run_os_check(tmp_path, "trixie", python_version="3.13")
assert "✓ NetworkManager is managing the network" in result.stdout
# --- check_system_compatibility.sh ---------------------------------------------
def run_compat(tmp_path: Path, release: str, **stub_args) -> subprocess.CompletedProcess:
env = _env(tmp_path, release, _stubs(tmp_path, **stub_args))
return subprocess.run(["bash", str(COMPAT)], capture_output=True, text=True, env=env)
@needs_bash
class TestCompatibilityCheck:
@pytest.mark.parametrize("release,python,label", [
("bookworm", "3.11", "Debian 12 (Bookworm)"),
("trixie", "3.13", "Debian 13 (Trixie)"),
])
def test_supported_release_is_reported_supported(self, tmp_path, release, python, label):
out = run_compat(tmp_path, release, python_version=python).stdout
assert f"Detected {label} - supported" in out
assert "Python version is supported (3.11-3.13)" in out
assert "not supported" not in out
assert "ships Python" not in out
@pytest.mark.parametrize("release", ["bullseye", "ubuntu"])
def test_unsupported_release_is_an_error(self, tmp_path, release):
result = run_compat(tmp_path, release)
assert result.returncode == 1
assert "is not supported - the installer requires Raspberry Pi OS Lite" in result.stdout
def test_python_below_the_floor_is_an_error(self, tmp_path):
result = run_compat(tmp_path, "bookworm", python_version="3.10")
assert result.returncode == 1
assert "Python 3.10 is too old - Python 3.11+ is required" in result.stdout
def test_dhcpcd_is_a_warning(self, tmp_path):
out = run_compat(tmp_path, "bookworm", network="dhcpcd").stdout
assert "dhcpcd manages the network" in out
assert "NetworkManager manages the network" not in out
def test_python_that_is_not_the_releases_own_is_flagged(self, tmp_path):
out = run_compat(tmp_path, "trixie", python_version="3.11").stdout
assert "Debian 13 (Trixie) ships Python 3.13, but python3 runs 3.11" in out
# --- things that must hold for both Python versions ----------------------------
def test_installer_scripts_do_not_hard_code_a_python_minor_version():
"""The services run /usr/bin/python3, which is 3.11 on Bookworm and 3.13
on Trixie; naming either one in an installer or a unit breaks the other."""
paths = [FIRST_TIME, *sorted((ROOT / "scripts" / "install").glob("*.sh")),
*sorted((ROOT / "systemd").glob("*.service"))]
offenders = []
for path in paths:
for n, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if line.lstrip().startswith("#"):
continue
if re.search(r"python3\.1[0-9]", line):
offenders.append(f"{path.relative_to(ROOT)}:{n}: {line.strip()}")
assert not offenders, "\n".join(offenders)
def _load_apt_installer():
spec = importlib.util.spec_from_file_location(
"install_dependencies_apt", ROOT / "scripts" / "install_dependencies_apt.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class TestAptFallbackRespectsThePins:
"""Step 7's apt-first fallback must not accept the releases' older apt
copies (Bookworm: Flask 2.2.2, Pillow 9.4; Trixie: Flask 3.1.1)."""
def test_floors_come_from_the_web_requirements(self):
mod = _load_apt_installer()
for name in ("flask", "werkzeug", "pillow", "requests", "psutil", "pytz", "freetype-py"):
assert mod.MIN_VERSIONS.get(name), f"no floor read for {name}"
assert mod.MIN_VERSIONS["freetype-py"] == (2, 5, 1)
@pytest.mark.parametrize("package,apt_version", [
("flask", (2, 2, 2)), # Bookworm
("flask", (3, 1, 1)), # Trixie
("PIL", (9, 4, 0)), # Bookworm python3-pil
("werkzeug", (2, 2, 2)),
("freetype-py", (2, 3, 0)),
])
def test_an_apt_copy_below_the_pin_does_not_count(self, monkeypatch, package, apt_version):
mod = _load_apt_installer()
monkeypatch.setattr(mod, "_installed_version_tuple", lambda dist: apt_version)
monkeypatch.setitem(sys.modules, mod.IMPORT_NAME_MAP.get(package, package), object())
assert mod.check_package_installed(package) is False
def test_a_version_at_the_pin_counts(self, monkeypatch):
mod = _load_apt_installer()
floor = mod.MIN_VERSIONS["flask"]
monkeypatch.setattr(mod, "_installed_version_tuple", lambda dist: floor)
monkeypatch.setitem(sys.modules, "flask", object())
assert mod.check_package_installed("flask") is True
def test_the_pillow_version_is_looked_up_under_its_distribution_name(self, monkeypatch):
mod = _load_apt_installer()
seen = []
monkeypatch.setattr(mod, "_installed_version_tuple", lambda dist: seen.append(dist) or (99,))
monkeypatch.setitem(sys.modules, "PIL", object())
assert mod.check_package_installed("PIL") is True
assert seen == ["Pillow"]
def test_pip_is_asked_for_pillow_not_pil(self, monkeypatch):
mod = _load_apt_installer()
calls = []
monkeypatch.setattr(mod, "_run", lambda cmd: calls.append(cmd) or (True, ""))
assert mod.install_via_pip("PIL") == (True, "")
assert calls[0][-1] == "Pillow"
+2 -2
View File
@@ -6,8 +6,8 @@ Complete / Web UI Access" summary. `reboot` returns at once and the script
carried on printing while the system went down, so the SSH session usually
dropped before the user saw the web UI address.
first_time_install.sh exits on anything but Raspberry Pi OS Trixie before it
parses its arguments, so the behavioural test runs only the tail of the
first_time_install.sh exits on anything but Raspberry Pi OS Bookworm or Trixie
before it parses its arguments, so the behavioural test runs only the tail of the
script -- from the summary to the end -- with systemctl, nmcli, hostname, ip
and reboot stubbed.
"""
+1 -1
View File
@@ -1,5 +1,5 @@
# LEDMatrix Web Interface Dependencies
# Compatible with Python 3.10, 3.11, 3.12, and 3.13
# Compatible with Python 3.11, 3.12 and 3.13; CI tests 3.11 and 3.13
# Tested on Raspbian OS 12 (Bookworm) and 13 (Trixie)
# Web framework