diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 7721e70e..e1e37510 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -101,7 +101,10 @@ python3 --version 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. +- "A desktop is running": use the Lite image, not the desktop one, or boot + to the console with `sudo systemctl set-default multi-user.target` and + reboot. Desktop packages that are installed but not running only produce a + warning, and the install continues. - "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). diff --git a/first_time_install.sh b/first_time_install.sh index ea494c12..a4af1a6b 100755 --- a/first_time_install.sh +++ b/first_time_install.sh @@ -86,29 +86,44 @@ if [ -r "$LM_OS_RELEASE_FILE" ]; then 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 + # Check for a desktop. A desktop only competes with the panel for CPU while + # it runs, so a running display manager stops the install; desktop packages + # or session files on a Pi that boots to the console are only a warning. + DESKTOP_RUNNING=0 + DESKTOP_INSTALLED=0 + # display-manager is the alias every Debian display manager registers. + for dm in display-manager lightdm gdm gdm3 sddm lxdm; do + if systemctl is-active --quiet "$dm" 2>/dev/null; then + DESKTOP_RUNNING=1 + fi + done # grep without -q: -q exits at the first match, dpkg then dies of SIGPIPE, # and pipefail turns a found desktop into "not found". - # Match installed package names from their start: the unanchored ".*kde" - # matched mid-word (libblockdev-* = "bloc-kde-v") on Lite, and `dpkg -l` - # lines also carry descriptions that could match. + # Desktop metapackages and session managers, matched as whole installed + # package names: an unanchored ".*kde" matched libblockdev-* ("bloc-kde-v"), + # and a "gnome" prefix matched standalone parts such as gnome-keyring. + # Trixie replaced raspberrypi-ui-mods with the rpd-*-core metapackages. + DESKTOP_PACKAGES='raspberrypi-ui-mods|rpd-wayland-core|rpd-x-core' + DESKTOP_PACKAGES+='|lxde|lxde-core|lxsession|xfce4|xfce4-session' + DESKTOP_PACKAGES+='|gnome-shell|gnome-session|kde-plasma-desktop|plasma-desktop' + DESKTOP_PACKAGES+='|plasma-workspace|task-desktop|task-[a-z0-9]+-desktop' if dpkg-query -W -f='${db:Status-Abbrev} ${binary:Package}\n' 2>/dev/null \ - | grep -E "^ii +(raspberrypi-ui-mods|lxde|xfce|gnome|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 - DESKTOP_DETECTED=1 + | grep -E "^ii +(${DESKTOP_PACKAGES})(:[a-z0-9]+)?$" >/dev/null; then + DESKTOP_INSTALLED=1 fi if [ -d /usr/share/raspberrypi-ui-mods ] || [ -d /usr/share/xsessions ]; then - DESKTOP_DETECTED=1 + DESKTOP_INSTALLED=1 fi - - if [ "$DESKTOP_DETECTED" -eq 1 ]; then - echo "✗ ERROR: Desktop environment detected - this script requires Raspberry Pi OS Lite" - echo " Please use Raspberry Pi OS Lite (not the full desktop version)" + + if [ "$DESKTOP_RUNNING" -eq 1 ]; then + echo "✗ ERROR: A desktop is running - this script requires Raspberry Pi OS Lite" + echo " Please use Raspberry Pi OS Lite (not the full desktop version), or boot" + echo " to the console: sudo systemctl set-default multi-user.target && sudo reboot" OS_CHECK_FAILED=1 + elif [ "$DESKTOP_INSTALLED" -eq 1 ]; then + echo "⚠ WARNING: Desktop packages are installed, but no desktop is running." + echo " Continuing. Keep the Pi booting to the console: a running desktop" + echo " competes with the LED panel for CPU and can make it flicker." else echo "✓ Lite version confirmed (no desktop environment)" fi diff --git a/test/test_install_os_support.py b/test/test_install_os_support.py index 95f1fe43..4a53d5da 100644 --- a/test/test_install_os_support.py +++ b/test/test_install_os_support.py @@ -50,9 +50,11 @@ def _stub(bin_dir: Path, name: str, body: str) -> None: path.chmod(0o755) -def _stubs(tmp_path: Path, python_version="3.11", network="NetworkManager") -> Path: +def _stubs(tmp_path: Path, python_version="3.11", network="NetworkManager", + active=(), packages=()) -> Path: """python3 reports ``python_version`` (None: not installed); systemctl - reports ``network`` as the only active unit; dpkg lists no desktop.""" + reports ``network`` and ``active`` as the only active units; dpkg-query + lists ``packages`` as installed (none by default, so no desktop).""" bin_dir = tmp_path / "bin" bin_dir.mkdir(exist_ok=True) if python_version is None: @@ -61,10 +63,14 @@ def _stubs(tmp_path: Path, python_version="3.11", network="NetworkManager") -> P 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') + units = "|".join(f'*"is-active --quiet {unit}"' for unit in (network, *active)) + _stub(bin_dir, "systemctl", f'case "$*" in {units}) exit 0 ;; esac\nexit 3\n') _stub(bin_dir, "dpkg", "exit 0\n") - _stub(bin_dir, "dpkg-query", "exit 1\n") + if packages: + listing = "".join(f"ii {name}\\n" for name in packages) + _stub(bin_dir, "dpkg-query", f'printf "{listing}"\n') + else: + _stub(bin_dir, "dpkg-query", "exit 1\n") _stub(bin_dir, "ping", "exit 0\n") return bin_dir @@ -149,20 +155,22 @@ class TestLibrary: # --- first_time_install.sh's OS check ------------------------------------------ -def _os_check_section() -> str: +def _os_check_section(marker_root: str = "/nonexistent") -> str: """first_time_install.sh from the OS check up to the next section, with - the desktop-marker directories pointed somewhere that cannot exist.""" + the desktop-marker directories moved under ``marker_root`` (by default + 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) + section = section.replace(marker, marker_root + marker) return section -def run_os_check(tmp_path: Path, release: str, **stub_args) -> subprocess.CompletedProcess: +def run_os_check(tmp_path: Path, release: str, marker_root: str = "/nonexistent", + **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" @@ -171,7 +179,7 @@ def run_os_check(tmp_path: Path, release: str, **stub_args) -> subprocess.Comple 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', + + _os_check_section(marker_root) + '\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) @@ -230,6 +238,47 @@ class TestInstallerOsCheck: result = run_os_check(tmp_path, "trixie", python_version="3.13") assert "✓ NetworkManager is managing the network" in result.stdout + # A running desktop stops the install; one that is only installed warns. + + @pytest.mark.parametrize("unit", ["display-manager", "lightdm", "gdm", "sddm"]) + def test_running_desktop_stops(self, tmp_path, unit): + result = run_os_check(tmp_path, "trixie", python_version="3.13", active=(unit,)) + assert result.returncode == 1, result.stdout + result.stderr + assert "A desktop is running" in result.stdout + assert "multi-user.target" in result.stdout + assert "SECTION-DONE" not in result.stdout + + @pytest.mark.parametrize("package", [ + "raspberrypi-ui-mods", "rpd-wayland-core", "rpd-x-core", "xfce4", + "lxde-core", "gnome-shell", "kde-plasma-desktop", "plasma-workspace:arm64", + "task-desktop", "task-mate-desktop", + ]) + def test_installed_desktop_that_is_not_running_warns(self, tmp_path, package): + result = run_os_check(tmp_path, "trixie", python_version="3.13", + packages=("bash", package)) + assert result.returncode == 0, result.stdout + result.stderr + assert "Desktop packages are installed, but no desktop is running" in result.stdout + assert "✓ OS requirements met" in result.stdout + + def test_desktop_session_files_warn(self, tmp_path): + (tmp_path / "markers" / "usr" / "share" / "xsessions").mkdir(parents=True) + result = run_os_check(tmp_path, "trixie", python_version="3.13", + marker_root=str(tmp_path / "markers")) + assert result.returncode == 0, result.stdout + result.stderr + assert "Desktop packages are installed, but no desktop is running" in result.stdout + + @pytest.mark.parametrize("packages", [ + # libblockdev contains "kde" mid-word; the old check stopped on it. + ("libblockdev-crypto3", "libblockdev3:arm64"), + ("gnome-keyring", "xfce4-terminal", "xfconf", "lxde-icon-theme", + "kde-cli-tools", "gnome-session-common", "task-ssh-server", "rpd-plym-splash"), + ]) + def test_lite_with_desktop_named_parts_is_lite(self, tmp_path, packages): + result = run_os_check(tmp_path, "trixie", python_version="3.13", packages=packages) + assert result.returncode == 0, result.stdout + result.stderr + assert "✓ Lite version confirmed" in result.stdout + assert "WARNING: Desktop" not in result.stdout + # --- check_system_compatibility.sh ---------------------------------------------