Files
Classroom_Desktop/tests/test-deployment-support.sh
T
ekklesiaandClaude Opus 5 f7d5aecd6c build: make the classroom desktop something a .deb installs
Building the package found two things reading the tree did not.

dpkg refused it outright: plasma-workspace already owns
/etc/xdg/plasmanotifyrc and friends, and two packages cannot own one
path. The room's settings move to a directory of their own with
/etc/environment.d putting it in front of /etc/xdg, which is KConfig's
own cascade rather than a file fight.

The postinst then failed on a chmod for a command that no longer
exists. It now walks what the package installed instead of naming
files, removes the login shell that used to start sway on tty1, and
enables SDDM and the audio service.

Verified by installing the built package on a fresh machine and
starting the session from it: restrictions in force, palette applied,
audio service enabled and running, panel supervision unconditional.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 09:06:53 -05:00

141 lines
6.5 KiB
Bash
Executable File

#!/bin/bash
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
python3 - "$ROOT" <<'PY'
import json
import re
import sys
from pathlib import Path
root = Path(sys.argv[1])
control = (root / "packaging/debian/control.template").read_text()
postinst = (root / "packaging/debian/postinst").read_text()
provision = (root / "scripts/classroom-provision").read_text()
units = "\n".join(path.read_text() for path in (root / "systemd").rglob("*.service"))
canary = (root / "update_canary.sh").read_text()
validation_script = (root / "tools/validate-trixie-release.sh").read_text()
layout = (root / "config/plasma/lookandfeel/contents/layouts/org.kde.plasma.desktop-layout.js").read_text()
rules = (root / "config/plasma/xdg/kwinrulesrc").read_text()
restrictions = (root / "config/plasma/xdg/kdeglobals").read_text()
supervision = (root / "systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf").read_text()
build = (root / "tools/build-release.sh").read_text()
policy = json.loads(
(root / "config/chrome/policies/managed/ekklesia-classroom.json").read_text()
)
passed = failed = 0
def check(label, condition):
global passed, failed
if condition:
print(f" ok {label}")
passed += 1
else:
print(f" FAIL {label}")
failed += 1
depends = re.search(r"^Depends: (.*)$", control, re.MULTILINE).group(1).split(", ")
print("1. Trixie package contract")
for package in (
"network-manager",
"cups",
"cups-client",
"cups-filters",
"foomatic-db-compressed-ppds",
):
check(f"depends on {package}", package in depends)
print()
print("2. networking service behavior")
check("NetworkManager is explicitly enabled", "systemctl enable NetworkManager.service || true" in postinst)
check("NetworkManager start is best effort", "systemctl start NetworkManager.service || true" in postinst)
check("online waiter is disabled", "systemctl disable NetworkManager-wait-online.service || true" in postinst)
check("no classroom NetworkManager config overrides ifupdown", not (root / "config/network").exists())
print()
print("3. printing service behavior")
check("CUPS service and socket are enabled", "systemctl enable cups.socket cups.service || true" in postinst)
check("CUPS start is best effort", "systemctl start cups.socket || true" in postinst)
check("automatic remote queue browsing is disabled", "systemctl disable --now cups-browsed.service || true" in postinst)
check("Avahi service and socket are disabled", "systemctl disable --now avahi-daemon.socket avahi-daemon.service || true" in postinst)
check("Avahi service and socket are masked", "systemctl mask avahi-daemon.socket avahi-daemon.service || true" in postinst)
check("printer setup is not a blocking boot dependency", "classroom-printer-setup" not in units)
check("printer setup runs automatically in provisioning", "/usr/local/bin/classroom-printer-setup" in provision)
check("printer provisioning is bounded", "--kill-after=5s 30s" in provision)
check("Chrome uses the system print dialog", policy.get("DisablePrintPreview") is True)
print()
print("4. secondary windows are the window manager's business")
# Chrome gives its main window and every window a page opens the same
# class, so a rule keyed on that class maximises the lesson's popup too.
# It was measured doing exactly that. The browser is told to start
# maximized instead, and no rule mentions it.
check("no window rule claims the browser", "google-chrome" not in rules)
check("the browser asks for its own first window", "--start-maximized" in
(root / "scripts/classroom-browser").read_text())
check("Wonder Ink asks for the same", "--start-maximized" in
(root / "scripts/classroom-wonderink").read_text())
check("the Chrome window arbiter is gone", not (root / "scripts/classroom-chrome-windows").exists())
check("and is not packaged", "classroom-chrome-windows" not in build)
print()
print("4b. the panel is never allowed to stay dead")
# Plasma restarts a crashed panel three times a minute and then gives up.
# Measured: dead on the third kill. These lines are what make it
# unconditional, and losing them would lose the room its only way around.
check("restart is unconditional", "Restart=always" in supervision)
check("the burst limit is lifted", "StartLimitIntervalSec=0" in supervision)
check("the drop-in is packaged", "plasma-plasmashell.service.d" in build)
print()
print("4b2. the session settings do not fight another package")
# plasma-workspace owns /etc/xdg/kdeglobals and friends. dpkg refused the
# package when the classroom tried to own them too, so the room's copies
# live in a directory of their own, in front of /etc/xdg.
check("settings install to the classroom's own directory",
"$STAGE/etc/ekklesia-classroom/xdg/" in build)
check("nothing is installed into /etc/xdg", "$STAGE/etc/xdg/" not in build)
check("and that directory is searched first",
"XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg" in
(root / "config/environment/90-ekklesia-classroom.conf").read_text())
check("the environment file is packaged",
"/etc/environment.d/90-ekklesia-classroom.conf" in build)
print()
print("4c. the teacher cannot reach a command line")
for restriction in ("run_command=false", "shell_access=false",
"action/lock_screen=false",
"plasma/plasmashell/unlockedDesktop=false"):
check(f"{restriction}", restriction in restrictions)
check("the restrictions are immutable", "[KDE Action Restrictions][$i]" in restrictions)
print()
print("4d. clicking a launcher never hides an application")
check("minimise on click is off", 'minimizeActiveTaskOnClick", false' in layout)
check("there is no minimise button", "ButtonsOnRight=FX" in
(root / "config/plasma/xdg/kwinrc").read_text())
print()
print("5. bounded Trixie validation")
check("container has an outer deadline", "720s" in validation_script)
check("package installs have deadlines", "candidate-install 360" in validation_script)
check("container service startup is suppressed", "/usr/sbin/policy-rc.d" in validation_script and "exit 101" in validation_script)
print()
print("6. canary release gate")
validation = canary.index('"$ROOT/tools/validate-trixie-release.sh"')
package_copy = canary.index('cp "$package_path"')
first_rsync = canary.index("rsync -av")
check("Trixie validation runs before the repository copy", validation < package_copy)
check("Trixie validation runs before any upload", validation < first_rsync)
print()
print(f"passed: {passed} failed: {failed}")
raise SystemExit(1 if failed else 0)
PY