Skip to content

Commit d98b479

Browse files
ChuckBuildsclaude
andauthored
fix(install): only a running desktop stops the install; detect desktops by metapackage (#781)
* fix(install): detect desktops by their metapackages, not name prefixes The Lite check matched any installed package starting with gnome/kde/ xfce/lxde, so standalone parts (gnome-keyring, xfce4-terminal, lxde-icon-theme) rejected a Lite system. Match whole names of desktop metapackages and session managers instead. Also catch desktops the prefixes missed: Raspberry Pi OS Trixie replaced raspberrypi-ui-mods with rpd-wayland-core / rpd-x-core, Debian tasksel desktops (task-*-desktop), and multi-arch names (plasma-workspace:arm64). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VZNWFWprcFYGfuf1JAyrBJ * docs(common): list espn_payload in the common README #749 added src/common/espn_payload.py without a summary row or section, so test_common_readme_lists_every_module fails on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VZNWFWprcFYGfuf1JAyrBJ (cherry picked from commit 8333f23) * fix(install): only a running desktop stops the install A desktop costs the panel CPU only while it runs, so a running display manager (checked with systemctl is-active, including the generic display-manager alias, instead of a grep -q pipe under pipefail) still stops the installer. Desktop packages or session files on a Pi that boots to the console now print a warning and the install continues. Adds installer OS-check tests for running, installed-only and Lite systems, including the libblockdev and gnome-keyring false positives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VZNWFWprcFYGfuf1JAyrBJ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 87f255b commit d98b479

3 files changed

Lines changed: 94 additions & 27 deletions

File tree

‎docs/TROUBLESHOOTING.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@ python3 --version
101101
Imager, choosing Raspberry Pi OS Lite (64-bit). Trixie is recommended;
102102
Bookworm (Legacy) also works. An in-place upgrade from Bullseye is not
103103
supported by Raspberry Pi and is not worth the risk.
104-
- "Desktop environment detected": use the Lite image, not the desktop one.
104+
- "A desktop is running": use the Lite image, not the desktop one, or boot
105+
to the console with `sudo systemctl set-default multi-user.target` and
106+
reboot. Desktop packages that are installed but not running only produce a
107+
warning, and the install continues.
105108
- "python3 is Python 3.x; LEDMatrix needs Python 3.11 or newer": something
106109
has replaced the system `python3`. Point it back at the OS's own Python
107110
(`/usr/bin/python3` should be 3.11 on Bookworm, 3.13 on Trixie).

‎first_time_install.sh‎

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -86,29 +86,44 @@ if [ -r "$LM_OS_RELEASE_FILE" ]; then
8686
OS_CHECK_FAILED=1
8787
fi
8888

89-
# Check if it's the Lite version (no desktop environment)
90-
# Check for desktop packages or desktop services
91-
DESKTOP_DETECTED=0
89+
# Check for a desktop. A desktop only competes with the panel for CPU while
90+
# it runs, so a running display manager stops the install; desktop packages
91+
# or session files on a Pi that boots to the console are only a warning.
92+
DESKTOP_RUNNING=0
93+
DESKTOP_INSTALLED=0
94+
# display-manager is the alias every Debian display manager registers.
95+
for dm in display-manager lightdm gdm gdm3 sddm lxdm; do
96+
if systemctl is-active --quiet "$dm" 2>/dev/null; then
97+
DESKTOP_RUNNING=1
98+
fi
99+
done
92100
# grep without -q: -q exits at the first match, dpkg then dies of SIGPIPE,
93101
# and pipefail turns a found desktop into "not found".
94-
# Match installed package names from their start: the unanchored ".*kde"
95-
# matched mid-word (libblockdev-* = "bloc-kde-v") on Lite, and `dpkg -l`
96-
# lines also carry descriptions that could match.
102+
# Desktop metapackages and session managers, matched as whole installed
103+
# package names: an unanchored ".*kde" matched libblockdev-* ("bloc-kde-v"),
104+
# and a "gnome" prefix matched standalone parts such as gnome-keyring.
105+
# Trixie replaced raspberrypi-ui-mods with the rpd-*-core metapackages.
106+
DESKTOP_PACKAGES='raspberrypi-ui-mods|rpd-wayland-core|rpd-x-core'
107+
DESKTOP_PACKAGES+='|lxde|lxde-core|lxsession|xfce4|xfce4-session'
108+
DESKTOP_PACKAGES+='|gnome-shell|gnome-session|kde-plasma-desktop|plasma-desktop'
109+
DESKTOP_PACKAGES+='|plasma-workspace|task-desktop|task-[a-z0-9]+-desktop'
97110
if dpkg-query -W -f='${db:Status-Abbrev} ${binary:Package}\n' 2>/dev/null \
98-
| grep -E "^ii +(raspberrypi-ui-mods|lxde|xfce|gnome|kde)" >/dev/null; then
99-
DESKTOP_DETECTED=1
100-
fi
101-
if systemctl list-units --type=service --state=running 2>/dev/null | grep -qE "lightdm|gdm3|sddm|lxdm"; then
102-
DESKTOP_DETECTED=1
111+
| grep -E "^ii +(${DESKTOP_PACKAGES})(:[a-z0-9]+)?$" >/dev/null; then
112+
DESKTOP_INSTALLED=1
103113
fi
104114
if [ -d /usr/share/raspberrypi-ui-mods ] || [ -d /usr/share/xsessions ]; then
105-
DESKTOP_DETECTED=1
115+
DESKTOP_INSTALLED=1
106116
fi
107-
108-
if [ "$DESKTOP_DETECTED" -eq 1 ]; then
109-
echo "✗ ERROR: Desktop environment detected - this script requires Raspberry Pi OS Lite"
110-
echo " Please use Raspberry Pi OS Lite (not the full desktop version)"
117+
118+
if [ "$DESKTOP_RUNNING" -eq 1 ]; then
119+
echo "✗ ERROR: A desktop is running - this script requires Raspberry Pi OS Lite"
120+
echo " Please use Raspberry Pi OS Lite (not the full desktop version), or boot"
121+
echo " to the console: sudo systemctl set-default multi-user.target && sudo reboot"
111122
OS_CHECK_FAILED=1
123+
elif [ "$DESKTOP_INSTALLED" -eq 1 ]; then
124+
echo "⚠ WARNING: Desktop packages are installed, but no desktop is running."
125+
echo " Continuing. Keep the Pi booting to the console: a running desktop"
126+
echo " competes with the LED panel for CPU and can make it flicker."
112127
else
113128
echo "✓ Lite version confirmed (no desktop environment)"
114129
fi

‎test/test_install_os_support.py‎

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@ def _stub(bin_dir: Path, name: str, body: str) -> None:
5050
path.chmod(0o755)
5151

5252

53-
def _stubs(tmp_path: Path, python_version="3.11", network="NetworkManager") -> Path:
53+
def _stubs(tmp_path: Path, python_version="3.11", network="NetworkManager",
54+
active=(), packages=()) -> Path:
5455
"""python3 reports ``python_version`` (None: not installed); systemctl
55-
reports ``network`` as the only active unit; dpkg lists no desktop."""
56+
reports ``network`` and ``active`` as the only active units; dpkg-query
57+
lists ``packages`` as installed (none by default, so no desktop)."""
5658
bin_dir = tmp_path / "bin"
5759
bin_dir.mkdir(exist_ok=True)
5860
if python_version is None:
@@ -61,10 +63,14 @@ def _stubs(tmp_path: Path, python_version="3.11", network="NetworkManager") -> P
6163
else:
6264
_stub(bin_dir, "python3", f'case "$*" in *"%d.%d.%d"*) echo "{python_version}.1" ;; '
6365
f'*) echo "{python_version}" ;; esac\n')
64-
_stub(bin_dir, "systemctl",
65-
f'case "$*" in *"is-active --quiet {network}") exit 0 ;; esac\nexit 3\n')
66+
units = "|".join(f'*"is-active --quiet {unit}"' for unit in (network, *active))
67+
_stub(bin_dir, "systemctl", f'case "$*" in {units}) exit 0 ;; esac\nexit 3\n')
6668
_stub(bin_dir, "dpkg", "exit 0\n")
67-
_stub(bin_dir, "dpkg-query", "exit 1\n")
69+
if packages:
70+
listing = "".join(f"ii {name}\\n" for name in packages)
71+
_stub(bin_dir, "dpkg-query", f'printf "{listing}"\n')
72+
else:
73+
_stub(bin_dir, "dpkg-query", "exit 1\n")
6874
_stub(bin_dir, "ping", "exit 0\n")
6975
return bin_dir
7076

@@ -149,20 +155,22 @@ def test_network_stack(self, tmp_path, active, expected):
149155

150156
# --- first_time_install.sh's OS check ------------------------------------------
151157

152-
def _os_check_section() -> str:
158+
def _os_check_section(marker_root: str = "/nonexistent") -> str:
153159
"""first_time_install.sh from the OS check up to the next section, with
154-
the desktop-marker directories pointed somewhere that cannot exist."""
160+
the desktop-marker directories moved under ``marker_root`` (by default
161+
somewhere that cannot exist)."""
155162
text = FIRST_TIME.read_text(encoding="utf-8").replace("\r\n", "\n")
156163
start = text.index("# Check OS version")
157164
end = text.index("# The user who ran the installer")
158165
section = text[start:end]
159166
for marker in ("/usr/share/raspberrypi-ui-mods", "/usr/share/xsessions"):
160167
assert marker in section
161-
section = section.replace(marker, "/nonexistent" + marker)
168+
section = section.replace(marker, marker_root + marker)
162169
return section
163170

164171

165-
def run_os_check(tmp_path: Path, release: str, **stub_args) -> subprocess.CompletedProcess:
172+
def run_os_check(tmp_path: Path, release: str, marker_root: str = "/nonexistent",
173+
**stub_args) -> subprocess.CompletedProcess:
166174
"""Run the OS check as the installer would, from a copy of the project
167175
layout so ``$(dirname "$0")/scripts/install/lib_os.sh`` resolves."""
168176
project = tmp_path / "project"
@@ -171,7 +179,7 @@ def run_os_check(tmp_path: Path, release: str, **stub_args) -> subprocess.Comple
171179
script = project / "first_time_install.sh"
172180
script.write_text("set -Eeuo pipefail\n"
173181
"trap 'echo ERR-TRAP line $LINENO >&2; exit 99' ERR\n"
174-
+ _os_check_section() + '\necho "SECTION-DONE"\n',
182+
+ _os_check_section(marker_root) + '\necho "SECTION-DONE"\n',
175183
encoding="utf-8", newline="\n")
176184
env = _env(tmp_path, release, _stubs(tmp_path, **stub_args))
177185
return subprocess.run(["bash", str(script)], capture_output=True, text=True, env=env)
@@ -230,6 +238,47 @@ def test_networkmanager_is_confirmed(self, tmp_path):
230238
result = run_os_check(tmp_path, "trixie", python_version="3.13")
231239
assert "✓ NetworkManager is managing the network" in result.stdout
232240

241+
# A running desktop stops the install; one that is only installed warns.
242+
243+
@pytest.mark.parametrize("unit", ["display-manager", "lightdm", "gdm", "sddm"])
244+
def test_running_desktop_stops(self, tmp_path, unit):
245+
result = run_os_check(tmp_path, "trixie", python_version="3.13", active=(unit,))
246+
assert result.returncode == 1, result.stdout + result.stderr
247+
assert "A desktop is running" in result.stdout
248+
assert "multi-user.target" in result.stdout
249+
assert "SECTION-DONE" not in result.stdout
250+
251+
@pytest.mark.parametrize("package", [
252+
"raspberrypi-ui-mods", "rpd-wayland-core", "rpd-x-core", "xfce4",
253+
"lxde-core", "gnome-shell", "kde-plasma-desktop", "plasma-workspace:arm64",
254+
"task-desktop", "task-mate-desktop",
255+
])
256+
def test_installed_desktop_that_is_not_running_warns(self, tmp_path, package):
257+
result = run_os_check(tmp_path, "trixie", python_version="3.13",
258+
packages=("bash", package))
259+
assert result.returncode == 0, result.stdout + result.stderr
260+
assert "Desktop packages are installed, but no desktop is running" in result.stdout
261+
assert "✓ OS requirements met" in result.stdout
262+
263+
def test_desktop_session_files_warn(self, tmp_path):
264+
(tmp_path / "markers" / "usr" / "share" / "xsessions").mkdir(parents=True)
265+
result = run_os_check(tmp_path, "trixie", python_version="3.13",
266+
marker_root=str(tmp_path / "markers"))
267+
assert result.returncode == 0, result.stdout + result.stderr
268+
assert "Desktop packages are installed, but no desktop is running" in result.stdout
269+
270+
@pytest.mark.parametrize("packages", [
271+
# libblockdev contains "kde" mid-word; the old check stopped on it.
272+
("libblockdev-crypto3", "libblockdev3:arm64"),
273+
("gnome-keyring", "xfce4-terminal", "xfconf", "lxde-icon-theme",
274+
"kde-cli-tools", "gnome-session-common", "task-ssh-server", "rpd-plym-splash"),
275+
])
276+
def test_lite_with_desktop_named_parts_is_lite(self, tmp_path, packages):
277+
result = run_os_check(tmp_path, "trixie", python_version="3.13", packages=packages)
278+
assert result.returncode == 0, result.stdout + result.stderr
279+
assert "✓ Lite version confirmed" in result.stdout
280+
assert "WARNING: Desktop" not in result.stdout
281+
233282

234283
# --- check_system_compatibility.sh ---------------------------------------------
235284

0 commit comments

Comments
 (0)